Upstream from paritytech master branch

This commit is contained in:
viktor 2018-01-19 22:26:10 +03:00
commit 199e7ec271
38 changed files with 4665 additions and 651 deletions

4
.gitignore vendored
View File

@ -51,4 +51,6 @@ examples/parity_ora-2/keys/OraclesPoA/dapps_history.json
examples/parity_ora-2/keys/OraclesPoA/address_book.json
jsTests/node_modules/
examples/db.toml
jsTests/package-lock.json
jsTests/package-lock.json
node_modules
compiled_contracts

View File

@ -1,16 +1,51 @@
sudo: false
language: rust
branches:
only:
- master
matrix:
fast_finish: false
include:
- rust: stable
- rust: beta
- rust: nightly
script:
- cargo test --all
- language: rust
rust: stable
cache: cargo
fast_finish: false
before_script:
- sudo add-apt-repository ppa:ethereum/ethereum -y
- sudo apt-get update -y
- sudo apt-get install solc -y
script:
- cargo test --all
- language: rust
rust: beta
cache: cargo
fast_finish: false
before_script:
- sudo add-apt-repository ppa:ethereum/ethereum -y
- sudo apt-get update -y
- sudo apt-get install solc -y
script:
- cargo test --all
- language: rust
rust: nightly
cache: cargo
fast_finish: false
before_script:
- sudo add-apt-repository ppa:ethereum/ethereum -y
- sudo apt-get update -y
- sudo apt-get install solc -y
script:
- cargo test --all
- language: node_js
node_js: node
cache: yarn
before_script:
- cd truffle
- yarn install
script:
- yarn run ci
after_script:
- cat coverage/lcov.info | yarn run coveralls
env:
global:
- secure: zdEco0QAPik4peDfWuLHHex67LVe3E7c5VJNx+7ygH1pt+mzgobKo8jgT7WuH70xPRA717txNaj/zYGj5EuBKLn+Tkw3feDjrISYRD7ZOXFm1urv53KDx8xh2QJld2fHOc4UWcQ1qqBOWWOR9donuOaRfdDSOpWjLhl14heMgsW3o5Q/V4HN//VPHQctzaCq6r5eerx82B6SSNQ7+42rESu37N0Plv8JtCswihCuoUsMuzbXGwGzafR8IVf5WJPB1WM1KpjdWHgZCCgIfdH6C9fJ1P4fd2Z7EQJ0PYwxRntPlONzUr5khGPldXn7Czwoq9Go4eOZaTwHizprI/KCXBXASXQ/Z7EsU2AKl90qvUHLDB9i4aa/eDrkzQGPQ+dkjNckdQaaucIKX/r8VDm7ZVefkLOgbzc1plE6/TXslAS/n0OoXUXydzueyqi8oeVEagt/nSYaR4t/8C10eC/6gjVF6X6mpgM6/p8eVrN8bltMa0KSDfRvhi3kU1Nmc5b3CWg+neWYYFPHak3GyFwh3uRC0LJroO+j+dkQZiEpSsMgthx69RBDjYvoi3T5FGwt5s/FfnOtcHM65M9sGubMW4DsVaI7OHt7FUnp5dlqxk6NGT68R/E1ZeCwr7Y4QCXr4agew5OpxTni4MK7aCVnmAtabNVLI4wKdCy2ULJWLsE=

128
README.md
View File

@ -1,11 +1,66 @@
# bridge
[![Build Status][travis-image]][travis-url]
[![Solidity Coverage Status][coveralls-image]][coveralls-url] (contracts only)
[travis-image]: https://travis-ci.org/paritytech/parity-bridge.svg?branch=master
[travis-url]: https://travis-ci.org/paritytech/parity-bridge
[coveralls-image]: https://coveralls.io/repos/github/paritytech/parity-bridge/badge.svg?branch=master
[coveralls-url]: https://coveralls.io/github/paritytech/parity-bridge?branch=master
Simple bridge between ValidatorSet-based parity chain (foreign) with any other Parity chain (home).
bridge between two ethereum blockchains, `home` and `foreign`.
### current functionality
the bridge allows users to deposit ether into a smart contract on `home` and get it on `foreign` in form of a token balance.
it also allows users to withdraw their tokens on `foreign` and get the equivalent ether on `home`.
on `foreign` users can freely transfer tokens between each other.
`foreign` is assumed to use PoA (proof of authority) consensus.
relays between the chains happen in a byzantine fault tolerant way using the authorities of `foreign`.
### next steps
1. deploy to bridge **ethereum** and **kovan** with the kovan authorities being the immutable set of bridge authorities
2. make bridge work with contract-based dynamic validator set
3. after kovan hardfork 2: deploy to kovan again with dynamic validator set
### eventual goals
connect ethereum to polkadot
### deposit ether into `HomeBridge` and get it in form of a token balance on `ForeignBridge`
`sender` deposits `value` into `HomeBridge`.
the `HomeBridge` fallback function emits `Deposit(sender, value)`.
for each `Deposit` event on `HomeBridge` every authority makes a transaction
`ForeignBridge.deposit(sender, value, transactionHash)`.
once there are `ForeignBridge.requiredSignatures` such transactions
with identical arguments and from distinct authorities then
`ForeignBridge.balances(sender)` is increased by `value` and
`ForeignBridge.Deposit(sender, value)` is emitted.
### withdraw balance on `ForeignBridge` and get it as ether on `home` chain
`sender` executes `ForeignBridge.transferHomeViaRelay(recipient, value)`
which checks and reduces `ForeignBridge.balances(sender)` by `value` and emits `ForeignBridge.Withdraw(recipient, value)`.
for each `ForeignBridge.Withdraw` every bridge authority creates a message containg
`value`, `recipient` and the `transactionHash` of the transaction containing the `ForeignBridge.Withdraw` event,
signs the message and makes a transaction `ForeignBridge.submitSignature(signature, message)`.
this collection of signatures on `foreign` is necessary because transactions are free
for authorities on `foreign`, since they are the authorities of `foreign`, but not free on `home`.
once `ForeignBridge.requiredSignatures` signatures by distinct authorities are collected
a `ForeignBridge.CollectedSignatures(authorityThatSubmittedLastSignature, messageHash)` event is emitted.
everyone (usually `authorityThatSubmittedLastSignature`) can then call `ForeignBridge.message(messageHash)` and
`ForeignBridge.signature(messageHash, 0..requiredSignatures)`
to look up the message and signatures and execute `HomeBridge.withdraw(vs, rs, ss, message)`
and complete the withdraw.
### transfer on `foreign`
`sender` executes `ForeignBridge.transferLocal(recipient, value)`
which checks and reduces `ForeignBridge.balances(sender)` and increases `ForeignBridge.balances(recipient)`
by `value`.
### build
@ -33,6 +88,8 @@ Options:
### configuration [file example](./examples/config.toml)
```toml
estimated_gas_cost_of_withdraw = 100000
[home]
account = "0x006e27b6a72e1f34c626762f3c4761547aff1421"
ipc = "/Users/marek/Library/Application Support/io.parity.ethereum/jsonrpc.ipc"
@ -62,6 +119,13 @@ home_deploy = { gas = 500000 }
foreign_deploy = { gas = 500000 }
```
#### options
- `estimated_gas_cost_of_withdraw` - how much gas a transaction to `HomeBridge.withdraw` consumes (**required**)
- currently recommended value: `100000`
- run [tools/estimate_gas_costs.sh](tools/estimate_gas_costs.sh) to compute an estimate
- see [recipient pays relay cost to relaying authority](#recipient-pays-relay-cost-to-relaying-authority) for why this config option is needed
#### home options
- `home.account` - authority address on the home (**required**)
@ -91,7 +155,7 @@ foreign_deploy = { gas = 500000 }
- `transaction.home_deploy.gas` - specify how much gas should be consumed by home contract deploy
- `transaction.home_deploy.gas_price` - specify gas price for home contract deploy
- `transaction.foreign_deploy.gas` - specify how much gas should be consumed by foreign contract deploy
- `transaction.foreign_deploy.gas_price` - specify gas price for home contract deploy
- `transaction.foreign_deploy.gas_price` - specify gas price for foreign contract deploy
- `transaction.deposit_relay.gas` - specify how much gas should be consumed by deposit relay
- `transaction.deposit_relay.gas_price` - specify gas price for deposit relay
- `transaction.withdraw_confirm.gas` - specify how much gas should be consumed by withdraw confirm
@ -124,7 +188,7 @@ checked_withdraw_confirm = 121
### example run
```
./target/debug/bridge --config examples/config.toml --database db.toml
./target/release/bridge --config examples/config.toml --database db.toml
```
- example run requires a parity instance running
@ -140,3 +204,61 @@ checked_withdraw_confirm = 121
### withdraw
![withdraw](./res/withdraw.png)
### truffle tests
[requires yarn to be installed](https://yarnpkg.com/lang/en/docs/install/)
```
cd truffle
yarn test
```
### recipient pays relay cost to relaying authority
a bridge `authority` has to pay for gas (`cost`) to execute `HomeBridge.withdraw` when
withdrawing `value` from `foreign` chain to `home` chain.
`value - cost` is transferred to the `recipient`. `cost` is transferred to the `authority`
executing `HomeBridge.withdraw`.
the `recipient` pays the relaying `authority` for the execution of the transaction.
that shuts down an attack that enabled exhaustion of authorities funds on `home`.
read on for a more thorough explanation.
parity-bridge connects a value-bearing ethereum blockchain `home`
(initally the ethereum foundation chain)
to a non-value-bearing PoA ethereum blockchain `foreign` (initally the kovan testnet).
value-bearing means that the ether on that chain has usable value in the sense that
in order to obtain it one has to either mine it (trade in electricity)
or trade in another currency.
non-value-bearing means that one can easily obtain a large amount of ether
on that chain for free.
through a faucet in the case of testnets for example.
the bridge authorities are also the validators of the `foreign` PoA chain.
transactions by the authorities are therefore free (gas price = 0) on `foreign`.
to execute a transaction on `home` a bridge authority has to spend ether to
pay for the gas.
this opened up an attack where a malicious user could
deposit a very small amount of wei on `HomeBridge`, get it relayed to `ForeignBridge`,
then spam `ForeignBridge.transfer` with `1` wei withdraws.
it would cost the attacker very little `home` chain wei and essentially
free `foreign` testnet wei to cause the authorities to spend orders of magnitude more wei
to relay the withdraw to `home` by executing `HomeBridge.withdraw`.
an attacker was able to exhaust bridge authorities funds on `home`.
to shut down this attack `HomeBridge.withdraw` was modified so
`value - cost` is transferred to the `recipient` and `cost` is transferred to the `authority`
doing the relay.
this way the `recipient` pays the relaying `authority` for the execution of the `withdraw` transaction.
if the value withdrawn is too low to pay for the relay at current gas prices then
bridge authorities will ignore it. one can think of it as value getting
spent entirely on paying the relay with no value left to pay out the recipient.
`HomeBridge.withdraw` is currently the only transaction bridge authorities execute on `home`.
care must be taken to secure future functions that bridge authorities will execute
on `home` in similar ways.

18
bridge/build.rs Normal file
View File

@ -0,0 +1,18 @@
use std::process::Command;
fn main() {
// rerun build script if bridge contract has changed.
// without this cargo doesn't since the bridge contract
// is outside the crate directories
println!("cargo:rerun-if-changed=../contracts/bridge.sol");
let exit_status = Command::new("solc")
.arg("--abi")
.arg("--bin")
.arg("--optimize")
.arg("--output-dir").arg("../compiled_contracts")
.arg("--overwrite")
.arg("../contracts/bridge.sol")
.status()
.unwrap_or_else(|e| panic!("Error compiling solidity contracts: {}", e));
assert!(exit_status.success(), "There was an error while compiling contracts code.");
}

View File

@ -45,7 +45,8 @@ impl<T: Transport + Clone> Future for Deploy<T> {
let main_data = self.app.home_bridge.constructor(
self.app.config.home.contract.bin.clone().0,
ethabi::util::pad_u32(self.app.config.authorities.required_signatures),
self.app.config.authorities.accounts.iter().map(|a| a.0.clone()).collect::<Vec<_>>()
self.app.config.authorities.accounts.iter().map(|a| a.0.clone()).collect::<Vec<_>>(),
ethabi::util::pad_u32(self.app.config.estimated_gas_cost_of_withdraw)
);
let test_data = self.app.foreign_bridge.constructor(
self.app.config.foreign.contract.bin.clone().0,

View File

@ -3,7 +3,7 @@ use futures::{Future, Stream, Poll};
use futures::future::{JoinAll, join_all, Join};
use tokio_timer::Timeout;
use web3::Transport;
use web3::types::{H256, Address, FilterBuilder, Log, Bytes, TransactionRequest};
use web3::types::{U256, H256, Address, FilterBuilder, Log, Bytes, TransactionRequest};
use ethabi::{RawLog, self};
use app::App;
use api::{self, LogStream, ApiCall};
@ -12,28 +12,36 @@ use util::web3_filter;
use database::Database;
use error::{self, Error};
/// returns a filter for `ForeignBridge.CollectedSignatures` events
fn collected_signatures_filter(foreign: &foreign::ForeignBridge, address: Address) -> FilterBuilder {
let filter = foreign.events().collected_signatures().create_filter();
web3_filter(filter, address)
}
/// payloads for calls to `ForeignBridge.signature` and `ForeignBridge.message`
/// to retrieve the signatures (v, r, s) and messages
/// which the withdraw relay process should later relay to `HomeBridge`
/// by calling `HomeBridge.withdraw(v, r, s, message)`
#[derive(Debug, PartialEq)]
struct RelayAssignment {
signature_payloads: Vec<Bytes>,
message_payload: Bytes,
}
fn signatures_payload(foreign: &foreign::ForeignBridge, signatures: u32, my_address: Address, log: Log) -> error::Result<Option<RelayAssignment>> {
fn signatures_payload(foreign: &foreign::ForeignBridge, required_signatures: u32, my_address: Address, log: Log) -> error::Result<Option<RelayAssignment>> {
// convert web3::Log to ethabi::RawLog since ethabi events can
// only be parsed from the latter
let raw_log = RawLog {
topics: log.topics.into_iter().map(|t| t.0).collect(),
data: log.data.0,
};
let collected_signatures = foreign.events().collected_signatures().parse_log(raw_log)?;
if collected_signatures.authority != my_address.0 {
// someone else will relay this transaction to home
// this authority is not responsible for relaying this transaction.
// someone else will relay this transaction to home.
return Ok(None);
}
let signature_payloads = (0..signatures).into_iter()
let signature_payloads = (0..required_signatures).into_iter()
.map(|index| ethabi::util::pad_u32(index))
.map(|index| foreign.functions().signature().input(collected_signatures.message_hash, index))
.map(Into::into)
@ -46,7 +54,19 @@ fn signatures_payload(foreign: &foreign::ForeignBridge, signatures: u32, my_addr
}))
}
fn withdraw_relay_payload(home: &home::HomeBridge, signatures: Vec<Bytes>, message: Bytes) -> Bytes {
/// returns the payload for a call to `HomeBridge.isMessageValueSufficientToCoverRelay(message)`
/// for the given `message`
fn message_value_sufficient_payload(home: &home::HomeBridge, message: &Bytes) -> Bytes {
assert_eq!(message.0.len(), 84, "ForeignBridge never accepts messages with len != 84 bytes; qed");
home
.functions()
.is_message_value_sufficient_to_cover_relay()
.input(message.0.clone()).into()
}
/// returns the payload for a transaction to `HomeBridge.withdraw(r, s, v, message)`
/// for the given `signatures` (r, s, v) and `message`
fn withdraw_relay_payload(home: &home::HomeBridge, signatures: &[Bytes], message: &Bytes) -> Bytes {
assert_eq!(message.0.len(), 84, "ForeignBridge never accepts messages with len != 84 bytes; qed");
let mut v_vec = Vec::new();
let mut r_vec = Vec::new();
@ -63,15 +83,22 @@ fn withdraw_relay_payload(home: &home::HomeBridge, signatures: Vec<Bytes>, messa
s_vec.push(s);
r_vec.push(r);
}
home.functions().withdraw().input(v_vec, r_vec, s_vec, message.0).into()
home.functions().withdraw().input(v_vec, r_vec, s_vec, message.0.clone()).into()
}
/// state of the withdraw relay state machine
pub enum WithdrawRelayState<T: Transport> {
Wait,
Fetch {
FetchMessagesSignatures {
future: Join<JoinAll<Vec<Timeout<ApiCall<Bytes, T::Out>>>>, JoinAll<Vec<JoinAll<Vec<Timeout<ApiCall<Bytes, T::Out>>>>>>>,
block: u64,
},
FetchMessageValueSufficient {
future: JoinAll<Vec<Timeout<ApiCall<Bytes, T::Out>>>>,
messages: Vec<Bytes>,
signatures: Vec<Vec<Bytes>>,
block: u64,
},
RelayWithdraws {
future: JoinAll<Vec<Timeout<ApiCall<H256, T::Out>>>>,
block: u64,
@ -149,19 +176,65 @@ impl<T: Transport> Stream for WithdrawRelay<T> {
.map(|calls| join_all(calls))
.collect::<Vec<_>>();
WithdrawRelayState::Fetch {
// wait for fetching of messages and signatures to complete
WithdrawRelayState::FetchMessagesSignatures {
future: join_all(message_calls).join(join_all(signature_calls)),
block: item.to,
}
},
WithdrawRelayState::Fetch { ref mut future, block } => {
WithdrawRelayState::FetchMessagesSignatures { ref mut future, block } => {
let (messages, signatures) = try_ready!(future.poll());
assert_eq!(messages.len(), signatures.len());
let app = &self.app;
let home_contract = &self.home_contract;
let relays = messages.into_iter().zip(signatures.into_iter())
.map(|(message, signatures)| withdraw_relay_payload(&app.home_bridge, signatures, message))
let message_value_sufficient_payloads = messages
.iter()
.map(|message| {
message_value_sufficient_payload(
&app.home_bridge,
message
)
})
.map(|payload| {
app.timer.timeout(
api::call(&app.connections.home, home_contract.clone(), payload),
app.config.home.request_timeout)
})
.collect::<Vec<_>>();
WithdrawRelayState::FetchMessageValueSufficient {
future: join_all(message_value_sufficient_payloads),
messages,
signatures,
block,
}
},
WithdrawRelayState::FetchMessageValueSufficient {
ref mut future,
ref messages,
ref signatures,
block
} => {
let message_value_sufficient = try_ready!(future.poll());
let app = &self.app;
let home_contract = &self.home_contract;
let relays = messages.into_iter()
.zip(signatures.into_iter())
.zip(message_value_sufficient.into_iter())
// ignore those messages that don't have sufficient
// value to pay for the relay gas cost
.filter(|&(_, ref is_message_value_sufficient)| {
// TODO [snd] this is ugly.
// in the future ethabi should return a bool
// for `is_message_value_sufficient`
// since the contract function returns a bool
U256::from(is_message_value_sufficient.0.as_slice()) == U256::from(1)
})
.map(|((message, signatures), _)| withdraw_relay_payload(&app.home_bridge, &signatures, message))
.map(|payload| TransactionRequest {
from: app.config.home.account.clone(),
to: Some(home_contract.clone()),
@ -178,6 +251,7 @@ impl<T: Transport> Stream for WithdrawRelay<T> {
app.config.home.request_timeout)
})
.collect::<Vec<_>>();
// wait for relays to complete
WithdrawRelayState::RelayWithdraws {
future: join_all(relays),
block,
@ -255,7 +329,7 @@ mod tests {
];
let message: Bytes = vec![0x33; 84].into();
let payload = withdraw_relay_payload(&home, signatures, message);
let payload = withdraw_relay_payload(&home, &signatures, &message);
let expected: Bytes = "9ce318f6000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000002111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000002111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000054333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333000000000000000000000000".from_hex().unwrap().into();
assert_eq!(expected, payload);
}

View File

@ -18,6 +18,7 @@ pub struct Config {
pub foreign: Node,
pub authorities: Authorities,
pub txs: Transactions,
pub estimated_gas_cost_of_withdraw: u32,
}
impl Config {
@ -42,6 +43,7 @@ impl Config {
required_signatures: config.authorities.required_signatures,
},
txs: config.transactions.map(Transactions::from_load_struct).unwrap_or_default(),
estimated_gas_cost_of_withdraw: config.estimated_gas_cost_of_withdraw,
};
Ok(result)
@ -141,6 +143,7 @@ mod load {
pub foreign: Node,
pub authorities: Authorities,
pub transactions: Option<Transactions>,
pub estimated_gas_cost_of_withdraw: u32,
}
#[derive(Deserialize)]
@ -194,6 +197,8 @@ mod tests {
#[test]
fn load_full_setup_from_str() {
let toml = r#"
estimated_gas_cost_of_withdraw = 100000
[home]
account = "0x1B68Cb0B50181FC4006Ce572cF346e596E51818b"
ipc = "/home.ipc"
@ -201,14 +206,14 @@ poll_interval = 2
required_confirmations = 100
[home.contract]
bin = "../contracts/HomeBridge.bin"
bin = "../compiled_contracts/HomeBridge.bin"
[foreign]
account = "0x0000000000000000000000000000000000000001"
ipc = "/foreign.ipc"
[foreign.contract]
bin = "../contracts/ForeignBridge.bin"
bin = "../compiled_contracts/ForeignBridge.bin"
[authorities]
accounts = [
@ -228,7 +233,7 @@ home_deploy = { gas = 20 }
account: "0x1B68Cb0B50181FC4006Ce572cF346e596E51818b".parse().unwrap(),
ipc: "/home.ipc".into(),
contract: ContractConfig {
bin: include_str!("../../contracts/HomeBridge.bin").from_hex().unwrap().into(),
bin: include_str!("../../compiled_contracts/HomeBridge.bin").from_hex().unwrap().into(),
},
poll_interval: Duration::from_secs(2),
request_timeout: Duration::from_secs(5),
@ -237,7 +242,7 @@ home_deploy = { gas = 20 }
foreign: Node {
account: "0x0000000000000000000000000000000000000001".parse().unwrap(),
contract: ContractConfig {
bin: include_str!("../../contracts/ForeignBridge.bin").from_hex().unwrap().into(),
bin: include_str!("../../compiled_contracts/ForeignBridge.bin").from_hex().unwrap().into(),
},
ipc: "/foreign.ipc".into(),
poll_interval: Duration::from_secs(1),
@ -251,7 +256,8 @@ home_deploy = { gas = 20 }
"0x0000000000000000000000000000000000000003".parse().unwrap(),
],
required_signatures: 2,
}
},
estimated_gas_cost_of_withdraw: 100_000,
};
expected.txs.home_deploy = TransactionConfig {
@ -266,19 +272,21 @@ home_deploy = { gas = 20 }
#[test]
fn load_minimal_setup_from_str() {
let toml = r#"
estimated_gas_cost_of_withdraw = 200000000
[home]
account = "0x1B68Cb0B50181FC4006Ce572cF346e596E51818b"
ipc = ""
[home.contract]
bin = "../contracts/HomeBridge.bin"
bin = "../compiled_contracts/HomeBridge.bin"
[foreign]
account = "0x0000000000000000000000000000000000000001"
ipc = ""
[foreign.contract]
bin = "../contracts/ForeignBridge.bin"
bin = "../compiled_contracts/ForeignBridge.bin"
[authorities]
accounts = [
@ -294,7 +302,7 @@ required_signatures = 2
account: "0x1B68Cb0B50181FC4006Ce572cF346e596E51818b".parse().unwrap(),
ipc: "".into(),
contract: ContractConfig {
bin: include_str!("../../contracts/HomeBridge.bin").from_hex().unwrap().into(),
bin: include_str!("../../compiled_contracts/HomeBridge.bin").from_hex().unwrap().into(),
},
poll_interval: Duration::from_secs(1),
request_timeout: Duration::from_secs(5),
@ -304,7 +312,7 @@ required_signatures = 2
account: "0x0000000000000000000000000000000000000001".parse().unwrap(),
ipc: "".into(),
contract: ContractConfig {
bin: include_str!("../../contracts/ForeignBridge.bin").from_hex().unwrap().into(),
bin: include_str!("../../compiled_contracts/ForeignBridge.bin").from_hex().unwrap().into(),
},
poll_interval: Duration::from_secs(1),
request_timeout: Duration::from_secs(5),
@ -317,7 +325,8 @@ required_signatures = 2
"0x0000000000000000000000000000000000000003".parse().unwrap(),
],
required_signatures: 2,
}
},
estimated_gas_cost_of_withdraw: 200_000_000,
};
let config = Config::load_from_str(toml).unwrap();

View File

@ -1,2 +1,2 @@
use_contract!(home, "HomeBridge", "../contracts/HomeBridge.abi");
use_contract!(foreign, "ForeignBridge", "../contracts/ForeignBridge.abi");
use_contract!(home, "HomeBridge", "../compiled_contracts/HomeBridge.abi");
use_contract!(foreign, "ForeignBridge", "../compiled_contracts/ForeignBridge.abi");

View File

@ -1 +0,0 @@
[]

View File

@ -1 +0,0 @@
60606040523415600e57600080fd5b603580601b6000396000f3006060604052600080fd00a165627a7a723058201dad5c43bba0bffc89ca5cf275cee04095a323df75058b9e5cfb736e54381f790029

View File

@ -1,246 +0,0 @@
[
{
"constant": true,
"inputs": [
{
"name": "hash",
"type": "bytes32"
},
{
"name": "index",
"type": "uint256"
}
],
"name": "signature",
"outputs": [
{
"name": "",
"type": "bytes"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "recipient",
"type": "address"
},
{
"name": "value",
"type": "uint256"
},
{
"name": "transactionHash",
"type": "bytes32"
}
],
"name": "deposit",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "balances",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "hash",
"type": "bytes32"
}
],
"name": "message",
"outputs": [
{
"name": "",
"type": "bytes"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "authorities",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "signature",
"type": "bytes"
},
{
"name": "message",
"type": "bytes"
}
],
"name": "submitSignature",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "requiredSignatures",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "recipient",
"type": "address"
},
{
"name": "value",
"type": "uint256"
},
{
"name": "externalTransfer",
"type": "bool"
}
],
"name": "transfer",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"name": "n",
"type": "uint256"
},
{
"name": "a",
"type": "address[]"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Withdraw",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "from",
"type": "address"
},
{
"indexed": false,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "authority",
"type": "address"
},
{
"indexed": false,
"name": "messageHash",
"type": "bytes32"
}
],
"name": "CollectedSignatures",
"type": "event"
}
]

File diff suppressed because one or more lines are too long

View File

@ -1,115 +0,0 @@
[
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "authorities",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "requiredSignatures",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "v",
"type": "uint8[]"
},
{
"name": "r",
"type": "bytes32[]"
},
{
"name": "s",
"type": "bytes32[]"
},
{
"name": "message",
"type": "bytes"
}
],
"name": "withdraw",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"name": "n",
"type": "uint256"
},
{
"name": "a",
"type": "address[]"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Withdraw",
"type": "event"
}
]

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
[]

View File

@ -1 +0,0 @@
60606040523415600e57600080fd5b603580601b6000396000f3006060604052600080fd00a165627a7a723058204971c18e0e1ddf69fded6644effcdb50c80ccc6d5a8aa81fe9fbfa53e1b68d100029

View File

@ -1,25 +0,0 @@
[
{
"constant": true,
"inputs": [
{
"name": "signature",
"type": "bytes"
},
{
"name": "message",
"type": "bytes"
}
],
"name": "signer",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]

View File

@ -1 +0,0 @@
6060604052341561000f57600080fd5b6105818061001e6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680631c7ede5f1461003d57600080fd5b6100d0600480803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509190803590602001908201803590602001908080601f01602080910402602001604051908101604052809392919081815260200183838082843782019150505050505091905050610112565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b600061011e8383610126565b905092915050565b6000806000806041865114151561013c57600080fd5b602086015192506040860151915060608601519050600161015c8661020e565b827f010000000000000000000000000000000000000000000000000000000000000090048585604051600081526020016040526000604051602001526040518085600019166000191681526020018460ff1660ff16815260200183600019166000191681526020018260001916600019168152602001945050505050602060405160208103908084039060008661646e5a03f115156101fa57600080fd5b505060206040510351935050505092915050565b600061021861052d565b6040805190810160405280601a81526020017f19457468657265756d205369676e6564204d6573736167653a0a00000000000081525090508061025b845161036e565b846040518084805190602001908083835b602083101515610291578051825260208201915060208101905060208303925061026c565b6001836020036101000a03801982511681845116808217855250505050505090500183805190602001908083835b6020831015156102e457805182526020820191506020810190506020830392506102bf565b6001836020036101000a03801982511681845116808217855250505050505090500182805190602001908083835b6020831015156103375780518252602082019150602081019050602083039250610312565b6001836020036101000a03801982511681845116808217855250505050505090500193505050506040518091039020915050919050565b610376610541565b61037e61052d565b60008061038961052d565b6000600860405180591061039a5750595b90808252806020026020018201604052509450600093505b60008714151561044957600a878115156103c857fe5b069250600a878115156103d757fe5b049650826030017f010000000000000000000000000000000000000000000000000000000000000002858580600101965081518110151561041457fe5b9060200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053506103b2565b836040518059106104575750595b90808252806020026020018201604052509150600090505b83811015610520578460018286030381518110151561048a57fe5b9060200101517f010000000000000000000000000000000000000000000000000000000000000090047f01000000000000000000000000000000000000000000000000000000000000000282828151811015156104e357fe5b9060200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a905350808060010191505061046f565b8195505050505050919050565b602060405190810160405280600081525090565b6020604051908101604052806000815250905600a165627a7a72305820fb76108a72fef4e281cb9e45df4aa8d8c33dcc5bff00ea6f7e5d9719a27745260029

View File

@ -1 +0,0 @@
[]

View File

@ -1 +0,0 @@
60606040523415600e57600080fd5b603580601b6000396000f3006060604052600080fd00a165627a7a723058200905d3e14b8b5697c0ffc99aac06fac68a87f03b29568419dadf089de31f7f130029

View File

@ -1,69 +1,163 @@
pragma solidity ^0.4.15;
pragma solidity ^0.4.17;
library Authorities {
function contains (address[] self, address value) internal returns (bool) {
for (uint i = 0; i < self.length; i++) {
if (self[i] == value) {
/// general helpers.
/// `internal` so they get compiled into contracts using them.
library Helpers {
/// returns whether `array` contains `value`.
function addressArrayContains(address[] array, address value) internal pure returns (bool) {
for (uint i = 0; i < array.length; i++) {
if (array[i] == value) {
return true;
}
}
return false;
}
}
/// Library used only to test Signer library via rpc calls
library SignerTest {
function signer (bytes signature, bytes message) constant returns (address) {
return Signer.signer(signature, message);
// returns the digits of `inputValue` as a string.
// example: `uintToString(12345678)` returns `"12345678"`
function uintToString(uint inputValue) internal pure returns (string) {
// figure out the length of the resulting string
uint length = 0;
uint currentValue = inputValue;
do {
length++;
currentValue /= 10;
} while (currentValue != 0);
// allocate enough memory
bytes memory result = new bytes(length);
// construct the string backwards
uint i = length - 1;
currentValue = inputValue;
do {
result[i--] = byte(48 + currentValue % 10);
currentValue /= 10;
} while (currentValue != 0);
return string(result);
}
}
library Utils {
function toString (uint256 v) internal returns (string str) {
// it is used only for small numbers
bytes memory reversed = new bytes(8);
uint i = 0;
while (v != 0) {
uint remainder = v % 10;
v = v / 10;
reversed[i++] = byte(48 + remainder);
}
bytes memory s = new bytes(i);
for (uint j = 0; j < i; j++) {
s[j] = reversed[i - j - 1];
}
str = string(s);
/// Library used only to test Helpers library via rpc calls
library HelpersTest {
function addressArrayContains(address[] array, address value) public pure returns (bool) {
return Helpers.addressArrayContains(array, value);
}
function uintToString(uint256 inputValue) public pure returns (string str) {
return Helpers.uintToString(inputValue);
}
}
library Signer {
function signer (bytes signature, bytes message) internal returns (address) {
// helpers for message signing.
// `internal` so they get compiled into contracts using them.
library MessageSigning {
function recoverAddressFromSignedMessage(bytes signature, bytes message) internal pure returns (address) {
require(signature.length == 65);
bytes32 r;
bytes32 s;
bytes1 v;
// solium-disable-next-line security/no-inline-assembly
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := mload(add(signature, 0x60))
}
return ecrecover(hash(message), uint8(v), r, s);
return ecrecover(hashMessage(message), uint8(v), r, s);
}
function hash (bytes message) internal returns (bytes32) {
function hashMessage(bytes message) internal pure returns (bytes32) {
bytes memory prefix = "\x19Ethereum Signed Message:\n";
return sha3(prefix, Utils.toString(message.length), message);
return keccak256(prefix, Helpers.uintToString(message.length), message);
}
}
contract HomeBridge {
using Authorities for address[];
/// Library used only to test MessageSigning library via rpc calls
library MessageSigningTest {
function recoverAddressFromSignedMessage(bytes signature, bytes message) public pure returns (address) {
return MessageSigning.recoverAddressFromSignedMessage(signature, message);
}
}
library Message {
// layout of message :: bytes:
// offset 0: 32 bytes :: uint (little endian) - message length
// offset 32: 20 bytes :: address - recipient address
// offset 52: 32 bytes :: uint (little endian) - value
// offset 84: 32 bytes :: bytes32 - transaction hash
// bytes 1 to 32 are 0 because message length is stored as little endian.
// mload always reads 32 bytes.
// so we can and have to start reading recipient at offset 20 instead of 32.
// if we were to read at 32 the address would contain part of value and be corrupted.
// when reading from offset 20 mload will read 12 zero bytes followed
// by the 20 recipient address bytes and correctly convert it into an address.
// this saves some storage/gas over the alternative solution
// which is padding address to 32 bytes and reading recipient at offset 32.
// for more details see discussion in:
// https://github.com/paritytech/parity-bridge/issues/61
function getRecipient(bytes message) internal pure returns (address) {
address recipient;
// solium-disable-next-line security/no-inline-assembly
assembly {
recipient := mload(add(message, 20))
}
return recipient;
}
function getValue(bytes message) internal pure returns (uint) {
uint value;
// solium-disable-next-line security/no-inline-assembly
assembly {
value := mload(add(message, 52))
}
return value;
}
function getTransactionHash(bytes message) internal pure returns (bytes32) {
bytes32 hash;
// solium-disable-next-line security/no-inline-assembly
assembly {
hash := mload(add(message, 84))
}
return hash;
}
}
/// Library used only to test Message library via rpc calls
library MessageTest {
function getRecipient(bytes message) public pure returns (address) {
return Message.getRecipient(message);
}
function getValue(bytes message) public pure returns (uint) {
return Message.getValue(message);
}
function getTransactionHash(bytes message) public pure returns (bytes32) {
return Message.getTransactionHash(message);
}
}
contract HomeBridge {
/// Number of authorities signatures required to withdraw the money.
///
/// Must be lesser than number of authorities.
uint public requiredSignatures;
/// The gas cost of calling `HomeBridge.withdraw`.
///
/// Is subtracted from `value` on withdraw.
/// recipient pays the relaying authority for withdraw.
/// this shuts down attacks that exhaust authorities funds on home chain.
uint public estimatedGasCostOfWithdraw;
/// Contract authorities.
address[] public authorities;
@ -77,57 +171,89 @@ contract HomeBridge {
event Withdraw (address recipient, uint value);
/// Multisig authority validation
modifier allAuthorities (uint8[] v, bytes32[] r, bytes32[] s, bytes message) {
var hash = Signer.hash(message);
modifier allAuthorities(uint8[] v, bytes32[] r, bytes32[] s, bytes message) {
var hash = MessageSigning.hashMessage(message);
var used = new address[](requiredSignatures);
require(requiredSignatures <= v.length);
for (uint i = 0; i < requiredSignatures; i++) {
var a = ecrecover(hash, v[i], r[i], s[i]);
require(authorities.contains(a));
require(!used.contains(a));
require(Helpers.addressArrayContains(authorities, a));
require(!Helpers.addressArrayContains(used, a));
used[i] = a;
}
_;
}
/// Constructor.
function HomeBridge (uint n, address[] a) {
require(n != 0);
require(n <= a.length);
requiredSignatures = n;
authorities = a;
function HomeBridge(
uint requiredSignaturesParam,
address[] authoritiesParam,
uint estimatedGasCostOfWithdrawParam
) public
{
require(requiredSignaturesParam != 0);
require(requiredSignaturesParam <= authoritiesParam.length);
requiredSignatures = requiredSignaturesParam;
authorities = authoritiesParam;
estimatedGasCostOfWithdraw = estimatedGasCostOfWithdrawParam;
}
/// Should be used to deposit money.
function () payable {
function () public payable {
Deposit(msg.sender, msg.value);
}
/// Used to withdrawn money from the contract.
/// to be called by authorities to check
/// whether they withdraw message should be relayed or whether it
/// is too low to cover the cost of calling withdraw and can be ignored
function isMessageValueSufficientToCoverRelay(bytes message) public view returns (bool) {
return Message.getValue(message) > getWithdrawRelayCost();
}
/// an upper bound to the cost of relaying a withdraw by calling HomeBridge.withdraw
function getWithdrawRelayCost() public view returns (uint) {
return estimatedGasCostOfWithdraw * tx.gasprice;
}
/// Used to withdraw money from the contract.
///
/// message contains:
/// withdrawal recipient (bytes20)
/// withdrawal value (uint)
/// foreign transaction hash (bytes32) // to avoid transaction duplication
function withdraw (uint8[] v, bytes32[] r, bytes32[] s, bytes message) allAuthorities(v, r, s, message) {
address recipient;
uint value;
bytes32 hash;
assembly {
recipient := mload(add(message, 0x20))
value := mload(add(message, 0x40))
hash := mload(add(message, 0x60))
}
///
/// NOTE that anyone can call withdraw provided they have the message and required signatures!
function withdraw(uint8[] v, bytes32[] r, bytes32[] s, bytes message) public allAuthorities(v, r, s, message) {
require(message.length == 84);
address recipient = Message.getRecipient(message);
uint value = Message.getValue(message);
bytes32 hash = Message.getTransactionHash(message);
// Duplicated withdraw
// The following two statements guard against reentry into this function.
// Duplicated withdraw or reentry.
require(!withdraws[hash]);
// Order of operations below is critical to avoid TheDAO-like bug
// Order of operations below is critical to avoid TheDAO-like re-entry bug
withdraws[hash] = true;
recipient.transfer(value);
Withdraw(recipient, value);
// this fails if `value` is not even enough to cover the relay cost.
// Authorities simply IGNORE withdraws where `value` cant relay cost.
// Think of it as `value` getting burned entirely on the relay with no value left to pay out the recipient.
require(isMessageValueSufficientToCoverRelay(message));
uint estimatedWeiCostOfWithdraw = getWithdrawRelayCost();
// charge recipient for relay cost
uint valueRemainingAfterSubtractingCost = value - estimatedWeiCostOfWithdraw;
// pay out recipient
recipient.transfer(valueRemainingAfterSubtractingCost);
// refund relay cost to relaying authority
msg.sender.transfer(estimatedWeiCostOfWithdraw);
Withdraw(recipient, valueRemainingAfterSubtractingCost);
}
}
@ -138,8 +264,6 @@ contract ERC20 {
}
contract ForeignBridge {
using Authorities for address[];
struct SignaturesCollection {
/// Signed message.
bytes message;
@ -182,16 +306,20 @@ contract ForeignBridge {
event CollectedSignatures(address authority, bytes32 messageHash);
/// Constructor.
function ForeignBridge(uint n, address[] a) {
require(n != 0);
require(n <= a.length);
requiredSignatures = n;
authorities = a;
function ForeignBridge(
uint requiredSignaturesParam,
address[] authoritiesParam
) public
{
require(requiredSignaturesParam != 0);
require(requiredSignaturesParam <= authoritiesParam.length);
requiredSignatures = requiredSignaturesParam;
authorities = authoritiesParam;
}
/// Multisig authority validation
modifier onlyAuthority () {
require(authorities.contains(msg.sender));
modifier onlyAuthority() {
require(Helpers.addressArrayContains(authorities, msg.sender));
_;
}
@ -215,12 +343,12 @@ contract ForeignBridge {
/// deposit recipient (bytes20)
/// deposit value (uint)
/// mainnet transaction hash (bytes32) // to avoid transaction duplication
function deposit (address recipient, uint value, bytes32 transactionHash) onlyAuthority() {
function deposit(address recipient, uint value, bytes32 transactionHash) public onlyAuthority() {
// Protection from misbehaing authority
var hash = sha3(recipient, value, transactionHash);
var hash = keccak256(recipient, value, transactionHash);
// Duplicated deposits
require(!deposits[hash].contains(msg.sender));
require(!Helpers.addressArrayContains(deposits[hash], msg.sender));
deposits[hash].push(msg.sender);
// TODO: this may cause troubles if requriedSignatures len is changed
@ -230,13 +358,36 @@ contract ForeignBridge {
}
}
/// Withdraw money
function withdraw(address recipient, uint value) public {
/// Transfer `value` from `msg.sender`s local balance (on `foreign` chain) to `recipient` on `home` chain.
///
/// immediately decreases `msg.sender`s local balance.
/// emits a `Withdraw` event which will be picked up by the bridge authorities.
/// bridge authorities will then sign off (by calling `submitSignature`) on a message containing `value`,
/// `recipient` and the `hash` of the transaction on `foreign` containing the `Withdraw` event.
/// once `requiredSignatures` are collected a `CollectedSignatures` event will be emitted.
/// an authority will pick up `CollectedSignatures` an call `HomeBridge.withdraw`
/// which transfers `value - relayCost` to `recipient` completing the transfer.
function transferHomeViaRelay(address recipient, uint value) public {
require(erc20token.allowance(msg.sender, this) >= value);
erc20token.transferFrom(msg.sender, this, value);
balances[msg.sender] -= value;
Withdraw(recipient, value);
}
/// Transfer `value` to `recipient` on this `foreign` chain.
///
/// does not affect `home` chain. does not do a relay.
function transferLocal(address recipient, uint value) public {
require(balances[msg.sender] >= value);
// fails if value == 0, or if there is an overflow
require(balances[recipient] + value > balances[recipient]);
balances[msg.sender] -= value;
balances[recipient] += value;
Transfer(msg.sender, recipient, value);
}
/// Should be used as sync tool
///
/// Message is a message that should be relayed to main chain once authorities sign it.
@ -245,33 +396,33 @@ contract ForeignBridge {
/// withdrawal recipient (bytes20)
/// withdrawal value (uint)
/// foreign transaction hash (bytes32) // to avoid transaction duplication
function submitSignature (bytes signature, bytes message) onlyAuthority() {
function submitSignature(bytes signature, bytes message) public onlyAuthority() {
// Validate submited signatures
require(Signer.signer(signature, message) == msg.sender);
require(MessageSigning.recoverAddressFromSignedMessage(signature, message) == msg.sender);
// Valid withdraw message must have 84 bytes
require(message.length == 84);
var hash = sha3(message);
var hash = keccak256(message);
// Duplicated signatures
require(!signatures[hash].signed.contains(msg.sender));
require(!Helpers.addressArrayContains(signatures[hash].signed, msg.sender));
signatures[hash].message = message;
signatures[hash].signed.push(msg.sender);
signatures[hash].signatures.push(signature);
// TODO: this may cause troubles if requriedSignatures len is changed
// TODO: this may cause troubles if requiredSignatures len is changed
if (signatures[hash].signed.length == requiredSignatures) {
CollectedSignatures(msg.sender, hash);
}
}
/// Get signature
function signature (bytes32 hash, uint index) constant returns (bytes) {
function signature(bytes32 hash, uint index) public view returns (bytes) {
return signatures[hash].signatures[index];
}
/// Get message
function message (bytes32 hash) constant returns (bytes) {
function message(bytes32 hash) public view returns (bytes) {
return signatures[hash].message;
}
}

View File

@ -1,3 +1,5 @@
estimated_gas_cost_of_withdraw = 100000
[home]
account = "0x006e27b6a72e1f34c626762f3c4761547aff1421"
ipc = "/Users/marek/Library/Application Support/io.parity.ethereum/jsonrpc.ipc"

View File

@ -135,7 +135,8 @@ macro_rules! test_app_stream {
authorities: Authorities {
accounts: $authorities_accs.iter().map(|a: &&str| a.parse().unwrap()).collect(),
required_signatures: $signatures,
}
},
estimated_gas_cost_of_withdraw: 100_000,
};
let app = App {
@ -153,6 +154,23 @@ macro_rules! test_app_stream {
let app = Arc::new(app);
let stream = $init_stream(app, &$db);
let res = stream.collect().wait();
assert_eq!(
home.expected_requests.len(),
home.requests.get(),
"home: expected {} requests but received only {}",
home.expected_requests.len(),
home.requests.get()
);
assert_eq!(
foreign.expected_requests.len(),
foreign.requests.get(),
"foreign: expected {} requests but received only {}",
foreign.expected_requests.len(),
foreign.requests.get()
);
assert_eq!($expected, res.unwrap());
}
}

View File

@ -5,8 +5,11 @@ extern crate tests;
use bridge::bridge::create_withdraw_relay;
// 1 signature required. relay polled twice.
// no CollectedSignatures on ForeignBridge.
// no relay.
test_app_stream! {
name => withdraw_relay_basic,
name => withdraw_relay_no_log_no_relay,
database => Database::default(),
home =>
account => "0x0000000000000000000000000000000000000001",
@ -40,8 +43,12 @@ test_app_stream! {
]
}
// 2 signatures required. relay polled twice.
// single CollectedSignatures log present. message value covers relay cost.
// authority not responsible.
// message is ignored.
test_app_stream! {
name => withdraw_relay_single_log_no_relay,
name => withdraw_relay_single_log_authority_not_responsible_no_relay,
database => Database::default(),
home =>
account => "0x0000000000000000000000000000000000000001",
@ -69,8 +76,11 @@ test_app_stream! {
]
}
// 2 signatures required. relay polled twice.
// single CollectedSignatures log present. message value covers relay cost.
// message gets relayed.
test_app_stream! {
name => withdraw_relay_single_log_relay,
name => withdraw_relay_single_log_sufficient_value_relay,
database => Database::default(),
home =>
account => "0x0000000000000000000000000000000000000001",
@ -88,6 +98,12 @@ test_app_stream! {
init => |app, db| create_withdraw_relay(app, db).take(1),
expected => vec![0x1005],
home_transport => [
// call to `isValueInMessageLargeEnoughToCoverWithdrawCost`
"eth_call" =>
req => r#"[{"data":"0x6498d59000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000054333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333000000000000000000000000","to":"0x0000000000000000000000000000000000000000"},"latest"]"#,
// respond with `true`
res => r#""0x0000000000000000000000000000000000000000000000000000000000000001""#;
// `withdraw`
"eth_sendTransaction" =>
req => r#"[{"data":"0x9ce318f6000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000002111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000002111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000054333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333000000000000000000000000","from":"0x0000000000000000000000000000000000000001","gas":"0x0","gasPrice":"0x0","to":"0x0000000000000000000000000000000000000000"}]"#,
res => r#""0x1db8f385535c0d178b8f40016048f3a3cffee8f94e68978ea4b277f57b638f0b""#;
@ -99,9 +115,11 @@ test_app_stream! {
"eth_getLogs" =>
req => r#"[{"address":["0x0000000000000000000000000000000000000000"],"fromBlock":"0x1","limit":null,"toBlock":"0x1005","topics":[["0xeb043d149eedb81369bec43d4c3a3a53087debc88d2525f13bfaa3eecda28b5c"],null,null,null]}]"#,
res => r#"[{"address":"0x0000000000000000000000000000000000000000","topics":["0xeb043d149eedb81369bec43d4c3a3a53087debc88d2525f13bfaa3eecda28b5c"],"data":"0x000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0","type":"","transactionHash":"0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364"}]"#;
// call to `message`
"eth_call" =>
req => r#"[{"data":"0x490a32c600000000000000000000000000000000000000000000000000000000000000f0","to":"0x0000000000000000000000000000000000000000"},"latest"]"#,
res => r#""0x333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333""#;
// calls to `signature`
"eth_call" =>
req => r#"[{"data":"0x1812d99600000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000000","to":"0x0000000000000000000000000000000000000000"},"latest"]"#,
res => r#""0x1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111""#;
@ -111,8 +129,60 @@ test_app_stream! {
]
}
// 2 signatures required. relay polled twice.
// single CollectedSignatures log present. message value doesn't cover cost.
// message is ignored.
test_app_stream! {
name => withdraw_relay_check_gas,
name => withdraw_relay_single_log_insufficient_value_no_relay,
database => Database::default(),
home =>
account => "0x0000000000000000000000000000000000000001",
confirmations => 12;
foreign =>
account => "0xaff3454fce5edbc8cca8697c15331677e6ebcccc",
confirmations => 12;
authorities =>
accounts => [
"0x0000000000000000000000000000000000000001",
"0x0000000000000000000000000000000000000002",
],
signatures => 2;
txs => Transactions::default(),
init => |app, db| create_withdraw_relay(app, db).take(1),
expected => vec![0x1005],
home_transport => [
// call to `isValueInMessageLargeEnoughToCoverWithdrawCost`
"eth_call" =>
req => r#"[{"data":"0x6498d59000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000054333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333000000000000000000000000","to":"0x0000000000000000000000000000000000000000"},"latest"]"#,
// respond with `false`
res => r#""0x0000000000000000000000000000000000000000000000000000000000000000""#;
// no `withdraw`
],
foreign_transport => [
"eth_blockNumber" =>
req => r#"[]"#,
res => r#""0x1011""#;
"eth_getLogs" =>
req => r#"[{"address":["0x0000000000000000000000000000000000000000"],"fromBlock":"0x1","limit":null,"toBlock":"0x1005","topics":[["0xeb043d149eedb81369bec43d4c3a3a53087debc88d2525f13bfaa3eecda28b5c"],null,null,null]}]"#,
res => r#"[{"address":"0x0000000000000000000000000000000000000000","topics":["0xeb043d149eedb81369bec43d4c3a3a53087debc88d2525f13bfaa3eecda28b5c"],"data":"0x000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0","type":"","transactionHash":"0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364"}]"#;
// call to `message`
"eth_call" =>
req => r#"[{"data":"0x490a32c600000000000000000000000000000000000000000000000000000000000000f0","to":"0x0000000000000000000000000000000000000000"},"latest"]"#,
res => r#""0x333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333""#;
// calls to `signature`
"eth_call" =>
req => r#"[{"data":"0x1812d99600000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000000","to":"0x0000000000000000000000000000000000000000"},"latest"]"#,
res => r#""0x1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111""#;
"eth_call" =>
req => r#"[{"data":"0x1812d99600000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000001","to":"0x0000000000000000000000000000000000000000"},"latest"]"#,
res => r#""0x2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222""#;
]
}
// like `withdraw_relay_single_log_sufficient_value_relay`
// but with explicit gas
test_app_stream! {
name => withdraw_relay_explicit_gas,
database => Database::default(),
home =>
account => "0x0000000000000000000000000000000000000001",
@ -136,6 +206,12 @@ test_app_stream! {
init => |app, db| create_withdraw_relay(app, db).take(1),
expected => vec![0x1005],
home_transport => [
// call to `isValueInMessageLargeEnoughToCoverWithdrawCost`
"eth_call" =>
req => r#"[{"data":"0x6498d59000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000054333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333000000000000000000000000","to":"0x0000000000000000000000000000000000000000"},"latest"]"#,
// true
res => r#""0x0000000000000000000000000000000000000000000000000000000000000001""#;
// `withdraw`
"eth_sendTransaction" =>
req => r#"[{"data":"0x9ce318f6000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000002111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000002111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000054333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333000000000000000000000000","from":"0x0000000000000000000000000000000000000001","gas":"0x10","gasPrice":"0x20","to":"0x0000000000000000000000000000000000000000"}]"#,
res => r#""0x1db8f385535c0d178b8f40016048f3a3cffee8f94e68978ea4b277f57b638f0b""#;
@ -147,9 +223,11 @@ test_app_stream! {
"eth_getLogs" =>
req => r#"[{"address":["0x0000000000000000000000000000000000000000"],"fromBlock":"0x1","limit":null,"toBlock":"0x1005","topics":[["0xeb043d149eedb81369bec43d4c3a3a53087debc88d2525f13bfaa3eecda28b5c"],null,null,null]}]"#,
res => r#"[{"address":"0x0000000000000000000000000000000000000000","topics":["0xeb043d149eedb81369bec43d4c3a3a53087debc88d2525f13bfaa3eecda28b5c"],"data":"0x000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0","type":"","transactionHash":"0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364"}]"#;
// call to `message`
"eth_call" =>
req => r#"[{"data":"0x490a32c600000000000000000000000000000000000000000000000000000000000000f0","to":"0x0000000000000000000000000000000000000000"},"latest"]"#,
res => r#""0x333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333""#;
// calls to `signature`
"eth_call" =>
req => r#"[{"data":"0x1812d99600000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000000","to":"0x0000000000000000000000000000000000000000"},"latest"]"#,
res => r#""0x1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111""#;
@ -159,8 +237,10 @@ test_app_stream! {
]
}
// like `withdraw_relay_single_log_sufficient_value_relay`
// but with explicit contract addresses
test_app_stream! {
name => withdraw_relay_single_contract_addresses,
name => withdraw_relay_single_explicit_contract_addresses,
database => Database {
home_contract_address: "0x00000000000000000000000000000000000000dd".parse().unwrap(),
foreign_contract_address: "0x00000000000000000000000000000000000000ee".parse().unwrap(),
@ -182,6 +262,12 @@ test_app_stream! {
init => |app, db| create_withdraw_relay(app, db).take(1),
expected => vec![0x1005],
home_transport => [
// call to `isValueInMessageLargeEnoughToCoverWithdrawCost`
"eth_call" =>
req => r#"[{"data":"0x6498d59000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000054333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333000000000000000000000000","to":"0x00000000000000000000000000000000000000dd"},"latest"]"#,
// true
res => r#""0x0000000000000000000000000000000000000000000000000000000000000001""#;
// `withdraw`
"eth_sendTransaction" =>
req => r#"[{"data":"0x9ce318f6000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000002111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000002111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000054333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333000000000000000000000000","from":"0x0000000000000000000000000000000000000001","gas":"0x0","gasPrice":"0x0","to":"0x00000000000000000000000000000000000000dd"}]"#,
res => r#""0x1db8f385535c0d178b8f40016048f3a3cffee8f94e68978ea4b277f57b638f0b""#;
@ -193,9 +279,11 @@ test_app_stream! {
"eth_getLogs" =>
req => r#"[{"address":["0x00000000000000000000000000000000000000ee"],"fromBlock":"0x1","limit":null,"toBlock":"0x1005","topics":[["0xeb043d149eedb81369bec43d4c3a3a53087debc88d2525f13bfaa3eecda28b5c"],null,null,null]}]"#,
res => r#"[{"address":"0x00000000000000000000000000000000000000ee","topics":["0xeb043d149eedb81369bec43d4c3a3a53087debc88d2525f13bfaa3eecda28b5c"],"data":"0x000000000000000000000000aff3454fce5edbc8cca8697c15331677e6ebcccc00000000000000000000000000000000000000000000000000000000000000f0","type":"","transactionHash":"0x884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a9424364"}]"#;
// call to `message`
"eth_call" =>
req => r#"[{"data":"0x490a32c600000000000000000000000000000000000000000000000000000000000000f0","to":"0x00000000000000000000000000000000000000ee"},"latest"]"#,
res => r#""0x333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333""#;
// calls to `signature`
"eth_call" =>
req => r#"[{"data":"0x1812d99600000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000000","to":"0x00000000000000000000000000000000000000ee"},"latest"]"#,
res => r#""0x1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111""#;

7
tools/estimate_gas_costs.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
# prints out estimated gas costs of contract functions.
# runs the tests which estimate and print out gas costs. then greps test output for gas costs.
cd truffle
yarn test | grep "estimated gas cost"

0
truffle/.soliumignore Normal file
View File

20
truffle/.soliumrc.json Normal file
View File

@ -0,0 +1,20 @@
{
"extends": "solium:all",
"plugins": [
"security"
],
"rules": {
"quotes": [
"error",
"double"
],
"indentation": [
"error",
4
],
"arg-overflow": [
"warning",
4
]
}
}

View File

@ -1,23 +1,26 @@
pragma solidity ^0.4.4;
contract Migrations {
address public owner;
uint public last_completed_migration;
address public owner;
uint public last_completed_migration;
modifier restricted() {
if (msg.sender == owner) _;
}
modifier restricted() {
if (msg.sender == owner) {
_;
}
}
function Migrations() {
owner = msg.sender;
}
function Migrations() public {
owner = msg.sender;
}
function setCompleted(uint completed) restricted {
last_completed_migration = completed;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address new_address) restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
function upgrade(address newAddress) public restricted {
Migrations upgraded = Migrations(newAddress);
upgraded.setCompleted(last_completed_migration);
}
}

32
truffle/package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "parity-bridge",
"version": "1.0.0",
"description": "Bridge between any two ethereum-based networks",
"license": "GPL-3.0",
"repository": {
"type": "git",
"url": "git+https://github.com/paritytech/parity-bridge.git"
},
"bugs": {
"url": "https://github.com/paritytech/parity-bridge/issues"
},
"homepage": "https://github.com/paritytech/parity-bridge",
"devDependencies": {
"concurrently": "^3.5.1",
"coveralls": "^3.0.0",
"ganache-cli": "^6.0.3",
"solidity-coverage": "^0.4.8",
"solium": "^1.1.2",
"truffle": "^4.0.4"
},
"dependencies": {},
"scripts": {
"ci": "concurrently \"yarn run solium\" \"yarn run truffle-with-rpc\" \"yarn run solidity-coverage\"",
"ganache": "ganache-cli --port 8547",
"solidity-coverage": "solidity-coverage",
"solium": "solium --dir contracts/",
"test": "yarn run truffle-with-rpc",
"truffle": "truffle test",
"truffle-with-rpc": "concurrently --success first --kill-others \"yarn run ganache\" \"yarn run truffle\""
}
}

View File

@ -1,4 +1,5 @@
var ForeignBridge = artifacts.require("ForeignBridge");
var helpers = require("./helpers/helpers");
contract('ForeignBridge', function(accounts) {
it("should deploy contract", function() {
@ -39,19 +40,19 @@ contract('ForeignBridge', function(accounts) {
var meta;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var user_account = accounts[2];
var userAccount = accounts[2];
var value = web3.toWei(1, "ether");
var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return meta.deposit(user_account, value, hash, { from: authorities[0] });
return meta.deposit(userAccount, value, hash, { from: authorities[0] });
}).then(function(result) {
assert.equal(1, result.logs.length, "Exactly one event should be created");
assert.equal("Deposit", result.logs[0].event, "Event name should be Deposit");
assert.equal(user_account, result.logs[0].args.recipient, "Event recipient should be transaction sender");
assert.equal(userAccount, result.logs[0].args.recipient, "Event recipient should be transaction sender");
assert.equal(value, result.logs[0].args.value, "Event value should match deposited ether");
return meta.balances.call(user_account);
return meta.balances.call(userAccount);
}).then(function(result) {
assert.equal(value, result, "Contract balance should change");
})
@ -61,105 +62,194 @@ contract('ForeignBridge', function(accounts) {
var meta;
var requiredSignatures = 2;
var authorities = [accounts[0], accounts[1]];
var user_account = accounts[2];
var userAccount = accounts[2];
var value = web3.toWei(1, "ether");
var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return meta.deposit(user_account, value, hash, { from: authorities[0] });
return meta.deposit(userAccount, value, hash, { from: authorities[0] });
}).then(function(result) {
assert.equal(0, result.logs.length, "No event should be created");
return meta.balances.call(user_account);
return meta.balances.call(userAccount);
}).then(function(result) {
assert.equal(web3.toWei(0, "ether"), result, "Contract balance should not change yet");
return meta.deposit(user_account, value, hash, { from: authorities[1] });
return meta.deposit(userAccount, value, hash, { from: authorities[1] });
}).then(function(result) {
assert.equal(1, result.logs.length, "Exactly one event should be created");
assert.equal("Deposit", result.logs[0].event, "Event name should be Deposit");
assert.equal(user_account, result.logs[0].args.recipient, "Event recipient should be transaction sender");
assert.equal(userAccount, result.logs[0].args.recipient, "Event recipient should be transaction sender");
assert.equal(value, result.logs[0].args.value, "Event value should match deposited ether");
return meta.balances.call(user_account);
return meta.balances.call(userAccount);
}).then(function(result) {
assert.equal(value, result, "Contract balance should change");
})
})
it("should not be possible to do same deposit twice for same authority", function() {
var meta;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var userAccount = accounts[2];
var value = web3.toWei(1, "ether");
var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return meta.deposit(userAccount, value, hash, { from: authorities[0] });
}).then(function(_) {
return meta.deposit(userAccount, value, hash, { from: authorities[0] });
}).then(function(result) {
assert(false, "doing same deposit twice from same authority should fail");
}, function(err) {
})
})
it("should not allow non-authorities to execute deposit", function() {
var meta;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var userAccount = accounts[2];
var value = web3.toWei(1, "ether");
var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return meta.deposit(userAccount, value, hash, { from: userAccount });
}).then(function(result) {
assert(false, "should fail");
}, function(err) {
})
})
it("should ignore misbehaving authority when confirming deposit", function() {
var meta;
var requiredSignatures = 2;
var authorities = [accounts[0], accounts[1], accounts[2]];
var user_account = accounts[3];
var invalid_value = web3.toWei(2, "ether");
var userAccount = accounts[3];
var invalidValue = web3.toWei(2, "ether");
var value = web3.toWei(1, "ether");
var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return meta.deposit(user_account, value, hash, { from: authorities[0] });
return meta.deposit(userAccount, value, hash, { from: authorities[0] });
}).then(function(result) {
assert.equal(0, result.logs.length, "No event should be created yet");
return meta.deposit(user_account, invalid_value, hash, { from: authorities[1] });
return meta.deposit(userAccount, invalidValue, hash, { from: authorities[1] });
}).then(function(result) {
assert.equal(0, result.logs.length, "Misbehaving authority should be ignored");
return meta.deposit(user_account, value, hash, { from: authorities[2] })
return meta.deposit(userAccount, value, hash, { from: authorities[2] })
}).then(function(result) {
assert.equal(1, result.logs.length, "Exactly one event should be created");
assert.equal("Deposit", result.logs[0].event, "Event name should be Deposit");
assert.equal(user_account, result.logs[0].args.recipient, "Event recipient should be transaction sender");
assert.equal(userAccount, result.logs[0].args.recipient, "Event recipient should be transaction sender");
assert.equal(value, result.logs[0].args.value, "Event value should match transaction value");
return meta.balances.call(user_account);
return meta.balances.call(userAccount);
}).then(function(result) {
assert.equal(value, result, "Contract balance should change");
})
})
it("should allow user to transfer value internally", function() {
it("should allow user to transfer value locally", function() {
var meta;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var user_account = accounts[2];
var user_account2 = accounts[3];
var value = web3.toWei(3, "ether");
var value2 = web3.toWei(1, "ether");
var userAccount = accounts[2];
var userAccount2 = accounts[3];
var user1InitialValue = web3.toWei(3, "ether");
var transferedValue = web3.toWei(1, "ether");
var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return meta.deposit(user_account, value, hash, { from: authorities[0] });
// top up balance so we can transfer
return meta.deposit(userAccount, user1InitialValue, hash, { from: authorities[0] });
}).then(function(result) {
return meta.transfer(user_account2, value2, false, { from: user_account });
return meta.transferLocal(userAccount2, transferedValue, { from: userAccount });
}).then(function(result) {
assert.equal(1, result.logs.length, "Exactly one event should be created");
assert.equal("Transfer", result.logs[0].event, "Event name should be Transfer");
assert.equal(user_account, result.logs[0].args.from, "Event from should be transaction sender");
assert.equal(user_account2, result.logs[0].args.to, "Event from should be transaction recipient");
assert.equal(value2, result.logs[0].args.value, "Event value should match transaction value");
assert.equal(userAccount, result.logs[0].args.from, "Event from should be transaction sender");
assert.equal(userAccount2, result.logs[0].args.to, "Event from should be transaction recipient");
assert.equal(transferedValue, result.logs[0].args.value, "Event value should match transaction value");
return Promise.all([
meta.balances.call(user_account),
meta.balances.call(user_account2)
meta.balances.call(userAccount),
meta.balances.call(userAccount2)
])
}).then(function(result) {
assert.equal(web3.toWei(2, "ether"), result[0]);
assert.equal(web3.toWei(1, "ether"), result[1]);
assert.equal(transferedValue, result[1]);
})
})
it("should not allow user to transfer value", function() {
it("should not allow user to transfer value they don't have either locally or to home", function() {
var meta;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var user_account = accounts[2];
var user_account2 = accounts[3];
var value = web3.toWei(3, "ether");
var value2 = web3.toWei(4, "ether");
var userAccount = accounts[2];
var recipientAccount = accounts[3];
var userValue = web3.toWei(3, "ether");
var transferedValue = web3.toWei(4, "ether");
var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return meta.deposit(user_account, value, hash, { from: authorities[0] });
return meta.deposit(userAccount, userValue, hash, { from: authorities[0] });
}).then(function(result) {
return meta.transfer(user_account2, value2, false, { from: user_account });
return meta.transferLocal(recipientAccount, transferedValue, { from: userAccount });
}).then(function(result) {
assert(false, "Transfer should fail");
assert(false, "transferLocal should fail");
}, function(err) {
return meta.transferHomeViaRelay(recipientAccount, transferedValue, { from: userAccount });
}).then(function(result) {
assert(false, "transferHomeViaRelay should fail");
}, function(err) {
})
})
it("should fail to transfer 0 value both locally and to home", function() {
var meta;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var userAccount = accounts[2];
var recipientAccount = accounts[3];
var userValue = web3.toWei(3, "ether");
var transferedValue = web3.toWei(0, "ether");
var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return meta.deposit(userAccount, userValue, hash, { from: authorities[0] });
}).then(function(result) {
return meta.transferLocal(recipientAccount, transferedValue, { from: userAccount });
}).then(function(result) {
assert(false, "transferLocal should fail");
}, function(err) {
return meta.transferHomeViaRelay(recipientAccount, transferedValue, { from: userAccount });
}).then(function(result) {
assert(false, "transferHomeViaRelay should fail");
}, function(err) {
})
})
it("should fail to transfer with value overflow both locally and to home", function() {
var meta;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var userAccount = accounts[2];
var recipientAccount = accounts[3];
var userValue = web3.toWei(3, "ether");
var transferedvalue = web3.toWei("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "wei");
var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return meta.deposit(userAccount, userValue, hash, { from: authorities[0] });
}).then(function(result) {
return meta.transferLocal(recipientAccount, transferedValue, { from: userAccount });
}).then(function(result) {
assert(false, "transferLocal should fail");
}, function(err) {
return meta.transferHomeViaRelay(recipientAccount, transferedValue, { from: userAccount });
}).then(function(result) {
assert(false, "transferHomeViaRelay should fail");
}, function(err) {
})
})
@ -168,24 +258,25 @@ contract('ForeignBridge', function(accounts) {
var meta;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var user_account = accounts[2];
var user_account2 = accounts[3];
var userAccount = accounts[2];
var userAccount2 = accounts[3];
var value = web3.toWei(3, "ether");
var value2 = web3.toWei(1, "ether");
var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return meta.deposit(user_account, value, hash, { from: authorities[0] });
// top up balance so we can transfer
return meta.deposit(userAccount, value, hash, { from: authorities[0] });
}).then(function(result) {
return meta.transfer(user_account2, value2, true, { from: user_account });
return meta.transferHomeViaRelay(userAccount2, value2, { from: userAccount });
}).then(function(result) {
assert.equal(1, result.logs.length, "Exactly one event should be created");
assert.equal("Withdraw", result.logs[0].event, "Event name should be Withdraw");
assert.equal(user_account2, result.logs[0].args.recipient, "Event recipient should be equal to transaction recipient");
assert.equal(userAccount2, result.logs[0].args.recipient, "Event recipient should be equal to transaction recipient");
assert.equal(value2, result.logs[0].args.value, "Event value should match transaction value");
return Promise.all([
meta.balances.call(user_account),
meta.balances.call(user_account2)
meta.balances.call(userAccount),
meta.balances.call(userAccount2)
])
}).then(function(result) {
assert.equal(web3.toWei(2, "ether"), result[0]);
@ -193,29 +284,6 @@ contract('ForeignBridge', function(accounts) {
})
})
function sign(address, data) {
return new Promise(function(resolve, reject) {
web3.eth.sign(address, data, function(err, result) {
if (err !== null) {
return reject(err);
} else {
return resolve(normalizeSignature(result));
//return resolve(result);
}
})
})
}
// geth && testrpc has different output of eth_sign than parity
// https://github.com/ethereumjs/testrpc/issues/243#issuecomment-326750236
function normalizeSignature(signature) {
// strip 0x
signature = signature.substr(2);
// increase v by 27...
return "0x" + signature.substr(0, 128) + (parseInt(signature.substr(128), 16) + 27).toString(16);
}
it("should successfully submit signature and trigger CollectedSignatures event", function() {
var meta;
var signature;
@ -224,7 +292,7 @@ contract('ForeignBridge', function(accounts) {
var message = "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return sign(authorities[0], message);
return helpers.sign(authorities[0], message);
}).then(function(result) {
signature = result;
return meta.submitSignature(result, message, { from: authorities[0] });
@ -249,7 +317,7 @@ contract('ForeignBridge', function(accounts) {
var message = "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return sign(authorities[0], message);
return helpers.sign(authorities[0], message);
}).then(function(result) {
return meta.submitSignature(result, message, { from: authorities[0] });
}).then(function(result) {
@ -268,10 +336,10 @@ contract('ForeignBridge', function(accounts) {
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return Promise.all([
sign(authorities[0], message),
sign(authorities[1], message),
sign(authorities[0], message2),
sign(authorities[1], message2),
helpers.sign(authorities[0], message),
helpers.sign(authorities[1], message),
helpers.sign(authorities[0], message2),
helpers.sign(authorities[1], message2),
]);
}).then(function(result) {
signatures_for_message.push(result[0]);
@ -322,7 +390,7 @@ contract('ForeignBridge', function(accounts) {
var message = "0x1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return sign(authorities[0], message);
return helpers.sign(authorities[0], message);
}).then(function(result) {
return meta.submitSignature(result, message, { from: authorities[0] });
}).then(function(result) {
@ -340,7 +408,7 @@ contract('ForeignBridge', function(accounts) {
var message2 = "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111112";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return sign(authorities[0], message);
return helpers.sign(authorities[0], message);
}).then(function(result) {
return meta.submitSignature(result, message2, { from: authorities[0] });
}).then(function(result) {
@ -357,7 +425,7 @@ contract('ForeignBridge', function(accounts) {
var message = "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111";
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return sign(authorities[0], message);
return helpers.sign(authorities[0], message);
}).then(function(result) {
return meta.submitSignature(result, message, { from: authorities[1] });
}).then(function(result) {
@ -369,21 +437,22 @@ contract('ForeignBridge', function(accounts) {
it("should not be possible to submit signature twice", function() {
var meta;
var requiredSignatures = 0;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var message = "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111";
var signature;
return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) {
meta = instance;
return sign(authorities[0], message);
}).then(function(result) {
return meta.submitSignature(result, message, { from: authorities[0] });
}).then(function(result) {
return meta.submitSignature(result, message, { from: authorities[0] });
return helpers.sign(authorities[0], message);
}).then(function(result) {
signature = result;
return meta.submitSignature(signature, message, { from: authorities[0] });
}).then(function(_) {
return meta.submitSignature(signature, message, { from: authorities[0] });
}).then(function(_) {
assert(false, "submitSignature should fail");
}, function (err) {
}, function (_) {
// nothing
})
})
})

87
truffle/test/helpers.js Normal file
View File

@ -0,0 +1,87 @@
// solidity Helpers library
var Helpers = artifacts.require("HelpersTest");
// testing helpers
var helpers = require("./helpers/helpers");
contract("Helpers", function() {
it("`addressArrayContains` should function correctly", function() {
var addresses = [
"0xd4f04f18d253f831e5b9bcfde7f20450562e03da",
"0x46ee1abbcd7215364174f84c3cbc4770d45966e9",
"0x5ef98710ff315ded660fe757bf7a861114287c1e",
];
var otherAddress = "0x006e27b6a72e1f34c626762f3c4761547aff1421";
var library;
return Helpers.new().then(function(instance) {
library = instance;
return library.addressArrayContains.call([], otherAddress);
}).then(function(result) {
assert.equal(result, false, "should return false for empty array");
return library.addressArrayContains.call([otherAddress], otherAddress);
}).then(function(result) {
assert.equal(result, true, "should return true for singleton array containing value");
return library.addressArrayContains.call([addresses[0]], addresses[1]);
}).then(function(result) {
assert.equal(result, false, "should return false for singleton array not containing value");
return library.addressArrayContains.call(addresses, addresses[0]);
}).then(function(result) {
assert.equal(result, true);
return library.addressArrayContains.call(addresses, addresses[1]);
}).then(function(result) {
assert.equal(result, true);
return library.addressArrayContains.call(addresses, addresses[2]);
}).then(function(result) {
assert.equal(result, true);
return library.addressArrayContains.call(addresses, otherAddress);
}).then(function(result) {
assert.equal(result, false);
})
})
it("`uintToString` should convert int to string", function() {
var numbersFrom1To100 = helpers.range(1, 101);
var library;
return Helpers.new().then(function(instance) {
library = instance;
return library.uintToString.call(0)
}).then(function(result) {
assert.equal(result, "0");
return Promise.all(numbersFrom1To100.map(function(number) {
return library.uintToString.call(number);
}));
}).then(function(result) {
assert.deepEqual(result, numbersFrom1To100.map(function(number) {
return number.toString();
}), "should convert numbers from 1 to 100 correctly");
return library.uintToString.estimateGas(1);
}).then(function(result) {
console.log("estimated gas cost of Helpers.uintToString(1)", result);
return library.uintToString.call(1234)
}).then(function(result) {
assert.equal(result, "1234");
return library.uintToString.call(12345678)
}).then(function(result) {
assert.equal(result, "12345678");
return library.uintToString.estimateGas(12345678)
}).then(function(result) {
console.log("estimated gas cost of Helpers.uintToString(12345678)", result);
return library.uintToString.call(web3.toBigNumber("131242344353464564564574574567456"));
}).then(function(result) {
assert.equal(result, "131242344353464564564574574567456");
})
})
})

View File

@ -0,0 +1,104 @@
// returns a Promise that resolves with a hex string that is the signature of
// `data` signed with the key of `address`
function sign(address, data) {
return new Promise(function(resolve, reject) {
web3.eth.sign(address, data, function(err, result) {
if (err !== null) {
return reject(err);
} else {
return resolve(normalizeSignature(result));
//return resolve(result);
}
})
})
}
module.exports.sign = sign;
// geth && testrpc has different output of eth_sign than parity
// https://github.com/ethereumjs/testrpc/issues/243#issuecomment-326750236
function normalizeSignature(signature) {
signature = strip0x(signature);
// increase v by 27...
return "0x" + signature.substr(0, 128) + (parseInt(signature.substr(128), 16) + 27).toString(16);
}
module.exports.normalizeSignature = normalizeSignature;
// strips leading "0x" if present
function strip0x(input) {
return input.replace(/^0x/, "");
}
module.exports.strip0x = strip0x;
// extracts and returns the `v`, `r` and `s` values from a `signature`.
// all inputs and outputs are hex strings with leading '0x'.
function signatureToVRS(signature) {
assert.equal(signature.length, 2 + 32 * 2 + 32 * 2 + 2);
signature = strip0x(signature);
var v = parseInt(signature.substr(64 * 2), 16);
var r = "0x" + signature.substr(0, 32 * 2);
var s = "0x" + signature.substr(32 * 2, 32 * 2);
return {v: v, r: r, s: s};
}
module.exports.signatureToVRS = signatureToVRS;
// returns BigNumber `num` converted to a little endian hex string
// that is exactly 32 bytes long.
// `num` must represent an unsigned integer
function bigNumberToPaddedBytes32(num) {
assert(web3._extend.utils.isBigNumber(num));
assert(num.isInteger());
assert(!num.isNegative());
var result = strip0x(num.toString(16));
while (result.length < 64) {
result = "0" + result;
}
return "0x" + result;
}
module.exports.bigNumberToPaddedBytes32 = bigNumberToPaddedBytes32;
// returns an promise that resolves to an object
// that maps `addresses` to their current balances
function getBalances(addresses) {
return Promise.all(addresses.map(function(address) {
return web3.eth.getBalance(address);
})).then(function(balancesArray) {
let addressToBalance = {};
addresses.forEach(function(address, index) {
addressToBalance[address] = balancesArray[index];
});
return addressToBalance;
})
}
module.exports.getBalances = getBalances;
// returns hex string of the bytes of the message
// composed from `recipient`, `value` and `transactionHash`
// that is relayed from `foreign` to `home` on withdraw
function createMessage(recipient, value, transactionHash) {
web3._extend.utils.isBigNumber(value);
recipient = strip0x(recipient);
assert.equal(recipient.length, 20 * 2);
transactionHash = strip0x(transactionHash);
assert.equal(transactionHash.length, 32 * 2);
var value = strip0x(bigNumberToPaddedBytes32(value));
assert.equal(value.length, 64);
var message = "0x" + recipient + value + transactionHash;
var expectedMessageLength = (20 + 32 + 32) * 2 + 2;
assert.equal(message.length, expectedMessageLength);
return message;
}
module.exports.createMessage = createMessage;
// returns array of integers progressing from `start` up to, but not including, `end`
function range(start, end) {
var result = [];
for (var i = start; i < end; i++) {
result.push(i);
}
return result;
}
module.exports.range = range;

View File

@ -1,12 +1,18 @@
var HomeBridge = artifacts.require("HomeBridge");
var helpers = require("./helpers/helpers");
contract('HomeBridge', function(accounts) {
it("should deploy contract", function() {
var meta;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var estimatedGasCostOfWithdraw = 0;
return HomeBridge.new(requiredSignatures, authorities).then(function(instance) {
return HomeBridge.new(
requiredSignatures,
authorities,
estimatedGasCostOfWithdraw
).then(function(instance) {
meta = instance;
return meta.requiredSignatures.call();
}).then(function(result) {
@ -26,9 +32,9 @@ contract('HomeBridge', function(accounts) {
})
})
it("should fail to deploy contract with to many signatures", function() {
it("should fail to deploy contract with too many signatures", function() {
var authorities = [accounts[0], accounts[1]];
return HomeBridge.new(3, authorities).then(function(_) {
return HomeBridge.new(3, authorities, 0).then(function(_) {
assert(false, "Contract should fail to deploy");
}, function(err) {
// do nothing
@ -39,20 +45,538 @@ contract('HomeBridge', function(accounts) {
var meta;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
let user_account = accounts[2];
var estimatedGasCostOfWithdraw = 0;
let userAccount = accounts[2];
let value = web3.toWei(1, "ether");
return HomeBridge.new(requiredSignatures, authorities).then(function(instance) {
return HomeBridge.new(
requiredSignatures,
authorities,
estimatedGasCostOfWithdraw
).then(function(instance) {
meta = instance;
return meta.sendTransaction({
value: value,
from: user_account
from: userAccount
})
}).then(function(result) {
assert.equal(1, result.logs.length, "Exactly one event should have been created");
assert.equal("Deposit", result.logs[0].event, "Event name should be Deposit");
assert.equal(user_account, result.logs[0].args.recipient, "Event recipient should be transaction sender");
assert.equal(userAccount, result.logs[0].args.recipient, "Event recipient should be transaction sender");
assert.equal(value, result.logs[0].args.value, "Event value should match deposited ether");
})
})
it("isMessageValueSufficientToCoverRelay should work correctly", function() {
var homeBridge;
var gasPrice;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var estimatedGasCostOfWithdraw = web3.toBigNumber(100000);
var userAccount = accounts[2];
var recipientAccount = accounts[3];
var estimatedWeiCostOfWithdraw;
var transactionHash = "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80";
return HomeBridge.new(
requiredSignatures,
authorities,
estimatedGasCostOfWithdraw
).then(function(instance) {
homeBridge = instance;
// do a test transaction so we can figure out the gasPrice
return homeBridge.sendTransaction({
value: 1,
from: userAccount
})
}).then(function(result) {
return web3.eth.getTransaction(result.tx);
}).then(function(tx) {
// getting the gas price dynamically instead of hardcoding it
// (which would require less code)
// is required because solidity-coverage sets it to 1
// and the usual default is 100000000000
gasPrice = tx.gasPrice;
estimatedWeiCostOfWithdraw = gasPrice.times(estimatedGasCostOfWithdraw);
return homeBridge.getWithdrawRelayCost();
}).then(function(result) {
assert(result.equals(estimatedWeiCostOfWithdraw), "getWithdrawRelayCost should return correct value");
var message = helpers.createMessage(recipientAccount, estimatedWeiCostOfWithdraw, transactionHash);
return homeBridge.isMessageValueSufficientToCoverRelay(message);
}).then(function(result) {
assert.equal(result, false, "exactly estimatedWeiCostOfWithdraw is not sufficient value");
var message = helpers.createMessage(recipientAccount, estimatedWeiCostOfWithdraw.plus(1), transactionHash);
return homeBridge.isMessageValueSufficientToCoverRelay(message);
}).then(function(result) {
assert.equal(result, true, "estimatedWeiCostOfWithdraw + 1 is sufficient value");
})
})
it("should allow correct withdraw without recipient paying for gas", function() {
var homeBridge;
var signature;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var estimatedGasCostOfWithdraw = 0;
var userAccount = accounts[2];
var recipientAccount = accounts[3];
var value = web3.toBigNumber(web3.toWei(1, "ether"));
var message = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80");
return HomeBridge.new(
requiredSignatures,
authorities,
estimatedGasCostOfWithdraw
).then(function(instance) {
homeBridge = instance;
// "charge" HomeBridge so we can withdraw later
return homeBridge.sendTransaction({
value: value,
from: userAccount
})
}).then(function(result) {
return helpers.sign(authorities[0], message);
}).then(function(result) {
signature = result;
var vrs = helpers.signatureToVRS(signature);
return homeBridge.withdraw.estimateGas(
[vrs.v],
[vrs.r],
[vrs.s],
message,
{from: authorities[0]}
);
}).then(function(result) {
console.log("estimated gas cost of HomeBridge.withdraw =", result);
var vrs = helpers.signatureToVRS(signature);
return homeBridge.withdraw(
[vrs.v],
[vrs.r],
[vrs.s],
message,
// anyone can call withdraw (provided they have the message and required signatures)
{from: userAccount}
);
}).then(function(result) {
assert.equal(1, result.logs.length, "Exactly one event should be created");
assert.equal("Withdraw", result.logs[0].event, "Event name should be Withdraw");
assert.equal(recipientAccount, result.logs[0].args.recipient, "Event recipient should match recipient in message");
assert(value.equals(result.logs[0].args.value), "Event value should match value in message");
})
})
it("should allow correct withdraw with recipient paying caller for gas", function() {
var homeBridge;
var initialBalances;
var signature;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var estimatedGasCostOfWithdraw = web3.toBigNumber(100000);
var actualGasCostOfWithdraw;
var gasPrice;
var transactionResult;
var relayCost;
var relayerAccount = accounts[2];
var recipientAccount = accounts[3];
var chargerAccount = accounts[4];
var value = web3.toBigNumber(web3.toWei(1, "ether"));
var message = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80");
return HomeBridge.new(
requiredSignatures,
authorities,
estimatedGasCostOfWithdraw
).then(function(instance) {
homeBridge = instance;
return helpers.getBalances(accounts);
}).then(function(result) {
initialBalances = result;
// "charge" HomeBridge so we can withdraw later
return homeBridge.sendTransaction({
value: value,
from: chargerAccount,
})
}).then(function(result) {
return helpers.sign(authorities[0], message);
}).then(function(result) {
signature = result;
var vrs = helpers.signatureToVRS(signature);
return homeBridge.withdraw(
[vrs.v],
[vrs.r],
[vrs.s],
message,
// anyone can call withdraw (provided they have the message and required signatures)
{ from: relayerAccount }
);
}).then(function(result) {
transactionResult = result;
actualGasCostOfWithdraw = web3.toBigNumber(result.receipt.gasUsed);
return web3.eth.getTransaction(result.tx);
}).then(function(tx) {
// getting the gas price dynamically instead of hardcoding it
// (which would require less code)
// is required because solidity-coverage sets it to 1
// and the usual default is 100000000000
gasPrice = tx.gasPrice;
relayCost = gasPrice.times(estimatedGasCostOfWithdraw);
assert.equal(1, transactionResult.logs.length, "Exactly one event should be created");
assert.equal("Withdraw", transactionResult.logs[0].event, "Event name should be Withdraw");
assert.equal(recipientAccount, transactionResult.logs[0].args.recipient, "Event recipient should match recipient in message");
assert(value.minus(relayCost).equals(transactionResult.logs[0].args.value), "Event value should match value in message minus relay cost");
return helpers.getBalances(accounts);
}).then(function(balances) {
let actualWeiCostOfWithdraw = actualGasCostOfWithdraw.times(gasPrice);
assert(
balances[recipientAccount].equals(
initialBalances[recipientAccount].plus(value.minus(relayCost))),
"Recipient received value minus relay cost");
assert(
balances[relayerAccount].equals(
initialBalances[relayerAccount]
.minus(actualWeiCostOfWithdraw)
.plus(relayCost)),
"Relayer received relay cost");
})
})
it("should revert withdraw if value is insufficient to cover costs", function() {
var homeBridge;
var initialBalances;
var signature;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var estimatedGasCostOfWithdraw = web3.toBigNumber(100000);
var relayerAccount = accounts[2];
var recipientAccount = accounts[3];
var chargerAccount = accounts[4];
var value = estimatedGasCostOfWithdraw;
var message = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80");
return HomeBridge.new(
requiredSignatures,
authorities,
estimatedGasCostOfWithdraw
).then(function(instance) {
homeBridge = instance;
return helpers.getBalances(accounts);
}).then(function(result) {
initialBalances = result;
// "charge" HomeBridge so we can withdraw later
return homeBridge.sendTransaction({
value: value,
from: chargerAccount,
})
}).then(function(result) {
return helpers.sign(authorities[0], message);
}).then(function(result) {
signature = result;
var vrs = helpers.signatureToVRS(signature);
return homeBridge.withdraw(
[vrs.v],
[vrs.r],
[vrs.s],
message,
// anyone can call withdraw (provided they have the message and required signatures)
{ from: relayerAccount }
);
}).then(function(result) {
assert(false, "withdraw if value <= estimatedGasCostOfWithdraw should fail");
}, function (err) {
// nothing
})
})
it("should allow second withdraw with different transactionHash but same recipient and value", function() {
var homeBridge;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
let estimatedGasCostOfWithdraw = 0;
var userAccount = accounts[2];
var recipientAccount = accounts[3];
var value = web3.toBigNumber(web3.toWei(1, "ether"));
var message1 = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80");
var message2 = helpers.createMessage(recipientAccount, value, "0x038c79eb958a13aa71996bac27c628f33f227288bd27d5e157b97e55e08fd2b3");
return HomeBridge.new(
requiredSignatures,
authorities,
estimatedGasCostOfWithdraw
).then(function(instance) {
homeBridge = instance;
// "charge" HomeBridge so we can withdraw later
return homeBridge.sendTransaction({
value: value.times(2),
from: userAccount
})
}).then(function(result) {
return helpers.sign(authorities[0], message1);
}).then(function(signature) {
var vrs = helpers.signatureToVRS(signature);
return homeBridge.withdraw(
[vrs.v],
[vrs.r],
[vrs.s],
message1,
{from: authorities[0]}
);
}).then(function(result) {
assert.equal(1, result.logs.length, "Exactly one event should be created");
assert.equal("Withdraw", result.logs[0].event, "Event name should be Withdraw");
assert.equal(recipientAccount, result.logs[0].args.recipient, "Event recipient should match recipient in message");
assert(value.equals(result.logs[0].args.value), "Event value should match value in message");
return helpers.sign(authorities[0], message2);
}).then(function(signature) {
var vrs = helpers.signatureToVRS(signature);
return homeBridge.withdraw(
[vrs.v],
[vrs.r],
[vrs.s],
message2,
{from: authorities[0]}
);
}).then(function(result) {
assert.equal(1, result.logs.length, "Exactly one event should be created");
assert.equal("Withdraw", result.logs[0].event, "Event name should be Withdraw");
assert.equal(recipientAccount, result.logs[0].args.recipient, "Event recipient should match recipient in message");
assert(value.equals(result.logs[0].args.value), "Event value should match value in message");
})
})
it("should not allow second withdraw (replay attack) with same transactionHash but different recipient and value", function() {
var homeBridge;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var estimatedGasCostOfWithdraw = 0;
var userAccount = accounts[2];
var recipientAccount = accounts[3];
var value = web3.toBigNumber(web3.toWei(1, "ether"));
var message1 = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80");
var message2 = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80");
return HomeBridge.new(
requiredSignatures,
authorities,
estimatedGasCostOfWithdraw
).then(function(instance) {
homeBridge = instance;
// "charge" HomeBridge so we can withdraw later
return homeBridge.sendTransaction({
value: value.times(2),
from: userAccount
})
}).then(function(result) {
return helpers.sign(authorities[0], message1);
}).then(function(signature) {
var vrs = helpers.signatureToVRS(signature);
return homeBridge.withdraw(
[vrs.v],
[vrs.r],
[vrs.s],
message1,
{from: authorities[0]}
);
}).then(function(result) {
assert.equal(1, result.logs.length, "Exactly one event should be created");
assert.equal("Withdraw", result.logs[0].event, "Event name should be Withdraw");
assert.equal(recipientAccount, result.logs[0].args.recipient, "Event recipient should match recipient in message");
assert(value.equals(result.logs[0].args.value), "Event value should match value in message");
return helpers.sign(authorities[0], message2);
}).then(function(signature) {
var vrs = helpers.signatureToVRS(signature);
return homeBridge.withdraw(
[vrs.v],
[vrs.r],
[vrs.s],
message2,
{from: authorities[0]}
);
}).then(function(result) {
assert(false, "should fail");
}, function (err) {
// nothing
})
})
it("withdraw without funds on HomeBridge should fail", function() {
var homeBridge;
var signature;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var estimatedGasCostOfWithdraw = 0;
var userAccount = accounts[2];
var recipientAccount = accounts[3];
var value = web3.toBigNumber(web3.toWei(1, "ether"));
var message = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80");
return HomeBridge.new(
requiredSignatures,
authorities,
estimatedGasCostOfWithdraw
).then(function(instance) {
homeBridge = instance;
return helpers.sign(authorities[0], message);
}).then(function(result) {
signature = result;
var vrs = helpers.signatureToVRS(signature);
return homeBridge.withdraw(
[vrs.v],
[vrs.r],
[vrs.s],
message.substr(0, 83),
{from: authorities[0]}
);
}).then(function(result) {
assert(false, "should fail");
}, function (err) {
// nothing
})
})
it("should not allow withdraw with message.length != 84", function() {
var homeBridge;
var signature;
var requiredSignatures = 1;
var authorities = [accounts[0], accounts[1]];
var estimatedGasCostOfWithdraw = 0;
var userAccount = accounts[2];
var recipientAccount = accounts[3];
var value = web3.toBigNumber(web3.toWei(1, "ether"));
var message = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80");
// make message too short
message = message.substr(0, 83);
return HomeBridge.new(
requiredSignatures,
authorities,
estimatedGasCostOfWithdraw
).then(function(instance) {
homeBridge = instance;
// "charge" HomeBridge so we can withdraw later
return homeBridge.sendTransaction({
value: value,
from: userAccount
})
}).then(function(result) {
return helpers.sign(authorities[0], message);
}).then(function(result) {
signature = result;
var vrs = helpers.signatureToVRS(signature);
return homeBridge.withdraw(
[vrs.v],
[vrs.r],
[vrs.s],
message,
// anyone can call withdraw (provided they have the message and required signatures)
{from: userAccount}
);
}).then(function(result) {
assert(false, "withdraw should fail");
}, function(err) {
})
})
it("withdraw should fail if not enough signatures are provided", function() {
var homeBridge;
var signature;
var requiredSignatures = 2;
var authorities = [accounts[0], accounts[1]];
var estimatedGasCostOfWithdraw = 0;
var userAccount = accounts[2];
var recipientAccount = accounts[3];
var value = web3.toBigNumber(web3.toWei(1, "ether"));
var message = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80");
return HomeBridge.new(
requiredSignatures,
authorities,
estimatedGasCostOfWithdraw
).then(function(instance) {
homeBridge = instance;
// "charge" HomeBridge so we can withdraw later
return homeBridge.sendTransaction({
value: value,
from: userAccount
})
}).then(function(result) {
return helpers.sign(authorities[0], message);
}).then(function(result) {
signature = result;
var vrs = helpers.signatureToVRS(signature);
return homeBridge.withdraw(
[vrs.v],
[vrs.r],
[vrs.s],
message,
// anyone can call withdraw (provided they have the message and required signatures)
{from: userAccount}
);
}).then(function(result) {
assert(false, "should fail");
}, function(err) {
})
})
it("withdraw should fail if duplicate signature is provided", function() {
var homeBridge;
var signature;
var requiredSignatures = 2;
var authorities = [accounts[0], accounts[1]];
var estimatedGasCostOfWithdraw = 0;
var userAccount = accounts[2];
var recipientAccount = accounts[3];
var value = web3.toBigNumber(web3.toWei(1, "ether"));
var message = helpers.createMessage(recipientAccount, value, "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80");
return HomeBridge.new(
requiredSignatures,
authorities,
estimatedGasCostOfWithdraw
).then(function(instance) {
homeBridge = instance;
// "charge" HomeBridge so we can withdraw later
return homeBridge.sendTransaction({
value: value,
from: userAccount
})
}).then(function(result) {
return helpers.sign(authorities[0], message);
}).then(function(result) {
signature = result;
var vrs = helpers.signatureToVRS(signature);
return homeBridge.withdraw(
[vrs.v, vrs.v],
[vrs.r, vrs.r],
[vrs.s, vrs.s],
message,
// anyone can call withdraw (provided they have the message and required signatures)
{from: userAccount}
);
}).then(function(result) {
assert(false, "should fail");
}, function(err) {
})
})
})

33
truffle/test/message.js Normal file
View File

@ -0,0 +1,33 @@
var Message = artifacts.require("MessageTest");
var helpers = require("./helpers/helpers");
contract("Message", function() {
var recipientAccount = "0x006e27b6a72e1f34c626762f3c4761547aff1421";
var value = web3.toBigNumber(web3.toWei(1, "ether"));
var transactionHash = "0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80";
var message = helpers.createMessage(recipientAccount, value, transactionHash);
it("should extract value", function() {
return Message.new().then(function(instance) {
return instance.getValue.call(message)
}).then(function(result) {
assert(result.equals(value));
})
})
it("should extract recipient", function() {
return Message.new().then(function(instance) {
return instance.getRecipient.call(message)
}).then(function(result) {
assert.equal(result, recipientAccount);
})
})
it("should extract transactionHash", function() {
return Message.new().then(function(instance) {
return instance.getTransactionHash.call(message)
}).then(function(result) {
assert.equal(result, transactionHash);
})
})
})

View File

@ -0,0 +1,40 @@
var MessageSigning = artifacts.require("MessageSigningTest");
contract("MessageSigning", function() {
it("should recover address from signed message", function() {
var signature = "0xb585c41f3cceb2ff9b5c033f2edbefe93415bde365489c989bad8cef3b18e38148a13e100608a29735d709fe708926d37adcecfffb32b1d598727028a16df5db1b";
var message = "0xdeadbeaf";
var account = "0x006e27b6a72e1f34c626762f3c4761547aff1421";
return MessageSigning.new().then(function(instance) {
return instance.recoverAddressFromSignedMessage.call(signature, message)
}).then(function(result) {
assert.equal(account, result);
})
})
it("should recover address from long signed message", function() {
var signature = "0x3c9158597e22fa43fcc6636399c560441808e1d8496de0108e401a2ad71022b15d1191cf3c96e06759601c8e00ce7f03f350c12b19d0a8ba3ab3c07a71063f2b1c";
var message = "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111";
var account = "0x006e27b6a72e1f34c626762f3c4761547aff1421";
return MessageSigning.new().then(function(instance) {
return instance.recoverAddressFromSignedMessage.call(signature, message)
}).then(function(result) {
assert.equal(account, result);
})
})
it("should fail to recover address from signature that is too short", function() {
var signature = "0x3c9158597e22fa43fcc6636399c560441808e1d8496de0108e401a2ad71022b15d1191cf3c96e06759601c8e00ce7f03f350c12b19d0a8ba3ab3c07a71063f2b";
var message = "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111";
var account = "0x006e27b6a72e1f34c626762f3c4761547aff1421";
return MessageSigning.new().then(function(instance) {
return instance.recoverAddressFromSignedMessage.call(signature, message)
}).then(function(result) {
assert(false, "should fail because signature is too short");
}, function(err) {
})
})
})

View File

@ -1,27 +0,0 @@
var Signer = artifacts.require("SignerTest");
contract("Signer", function() {
it("should validate signature", function() {
var signature = "0xb585c41f3cceb2ff9b5c033f2edbefe93415bde365489c989bad8cef3b18e38148a13e100608a29735d709fe708926d37adcecfffb32b1d598727028a16df5db1b";
var message = "0xdeadbeaf";
var account = "0x006e27b6a72e1f34c626762f3c4761547aff1421";
return Signer.new().then(function(instance) {
return instance.signer.call(signature, message)
}).then(function(result) {
assert.equal(account, result);
})
})
it("should validate signature for long message", function() {
var signature = "0x3c9158597e22fa43fcc6636399c560441808e1d8496de0108e401a2ad71022b15d1191cf3c96e06759601c8e00ce7f03f350c12b19d0a8ba3ab3c07a71063f2b1c";
var message = "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111";
var account = "0x006e27b6a72e1f34c626762f3c4761547aff1421";
return Signer.new().then(function(instance) {
return instance.signer.call(signature, message)
}).then(function(result) {
assert.equal(account, result);
})
})
})

View File

@ -2,8 +2,8 @@ module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*" // Match any network id
port: 8547,
network_id: "*", // Match any network id
}
}
};

2997
truffle/yarn.lock Normal file

File diff suppressed because it is too large Load Diff