diff --git a/Cargo.lock b/Cargo.lock index b6a6cff..26ca6eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,7 +329,6 @@ dependencies = [ "futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 7.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "pretty_assertions 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-hex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-core 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/README.md b/README.md index 993325c..9f8cae1 100644 --- a/README.md +++ b/README.md @@ -8,84 +8,118 @@ [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 -bridge between two ethereum blockchains, `home` and `foreign`. +the bridge is an +[ERC20 token](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md) +contract that is backed by ether on **another** ethereum blockchain. + +users can convert ether +one one chain into the same amount of ERC20 tokens on the other and back. +the bridge securely relays these conversions. + +**the bridge can solve scaling issues:** +by deploying a [proof-of-authority](https://paritytech.github.io/wiki/Proof-of-Authority-Chains.html) +network and bridging it to mainnet users can convert their mainnet ether +into ERC20 tokens on the PoA chain +and there transfer them with much lower transaction fees, +faster block times and unaffected by mainnet congestion. + +the users can withdraw their tokens worth of ether on the mainnet at any point. + +parity is using the bridge project to prototype +the system that will eventually connect ethereum and other non-parachains to +[polkadot](https://polkadot.io/). + +### next steps + +1. deploy to bridge **ethereum** and **kovan** with the kovan authorities being the fixed set of bridge authorities +2. make the bridge work with contract-based dynamic validator sets +3. after kovan hardfork 2: deploy to kovan again with dynamic validator set ### 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. +the bridge connects two chains `home` and `foreign`. + +when users deposit ether into the `HomeBridge` contract on `home` +they get the same amount of ERC20 tokens on `foreign`. + +[they can use `ForeignBridge` as they would use any ERC20 token.](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md) + +to convert their `foreign` ERC20 into ether on `home` +users can always call `ForeignBridge.transferHomeViaRelay(homeRecipientAddress, value)`. `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` +### highlevel explanation of home ether -> foreign ERC20 relay `sender` deposits `value` into `HomeBridge`. the `HomeBridge` fallback function emits `Deposit(sender, value)`. -for each `Deposit` event on `HomeBridge` every authority makes a transaction + +for each `Deposit` event on `HomeBridge` every authority executes `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. +`ForeignBridge.balanceOf(sender)` is increased by `value`. -### withdraw balance on `ForeignBridge` and get it as ether on `home` chain +### highlevel explanation of foreign ERC20 -> home ether relay `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)`. +signs the message and executes `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` +`HomeBridge.withdraw(vs, rs, ss, message)` recovers the addresses from the signatures, +checks that enough authorities in its authority list have signed and +finally transfers `value` ether ([minus the relay gas costs](#recipient-pays-relay-cost-to-relaying-authority)) +to `recipient`. -`sender` executes `ForeignBridge.transferLocal(recipient, value)` -which checks and reduces `ForeignBridge.balances(sender)` and increases `ForeignBridge.balances(recipient)` -by `value`. +### run truffle smart contract tests + +requires `yarn` to be `$PATH`. [installation instructions](https://yarnpkg.com/lang/en/docs/install/) + +``` +cd truffle +yarn test +``` ### build -requires `solc` to be in `$PATH`. [installation instructions](https://solidity.readthedocs.io/en/develop/installing-solidity.html) +requires `rust` and `cargo`: [installation instructions.](https://www.rust-lang.org/en-US/install.html) + +requires `solc` to be in `$PATH`: [installation instructions.](https://solidity.readthedocs.io/en/develop/installing-solidity.html) + +assuming you've cloned the bridge (`git clone git@github.com:paritytech/parity-bridge.git`) +and are in the project directory (`cd parity-bridge`) run: ``` cargo build -p bridge-cli --release ``` -### cli options +to install copy `../target/release/bridge` into a folder that's in your `$PATH`. + +### run ``` -Ethereum-Kovan bridge. - Copyright 2017 Parity Technologies (UK) Limited - -Usage: - bridge --config --database - bridge -h | --help - -Options: - -h, --help Display help message and exit. +bridge --config config.toml --database db.toml ``` - `--config` - location of the configuration file. configuration file must exist -- `--database` - location of the database file. if there is no file at specified location, new bridge contracts will be deployed and new database will be created +- `--database` - location of the database file. + if there is no file at specified location, new bridge contracts will be deployed + and new database will be created ### configuration [file example](./examples/config.toml) @@ -207,15 +241,6 @@ checked_withdraw_confirm = 121 ![withdraw](./res/withdraw.png) -### truffle tests - -requires `yarn` to be `$PATH`. [installation instructions](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 diff --git a/bridge/src/bridge/deposit_relay.rs b/bridge/src/bridge/deposit_relay.rs index 1a83cf2..ae25695 100644 --- a/bridge/src/bridge/deposit_relay.rs +++ b/bridge/src/bridge/deposit_relay.rs @@ -73,6 +73,7 @@ impl Stream for DepositRelay { let next_state = match self.state { DepositRelayState::Wait => { let item = try_stream!(self.logs.poll()); + info!("got {} new deposits to relay", item.logs.len()); let deposits = item.logs .into_iter() .map(|log| deposit_relay_payload(&self.app.home_bridge, &self.app.foreign_bridge, log)) @@ -95,6 +96,7 @@ impl Stream for DepositRelay { }) .collect::>(); + info!("relaying {} deposits", deposits.len()); DepositRelayState::RelayDeposits { future: join_all(deposits), block: item.to, @@ -102,6 +104,7 @@ impl Stream for DepositRelay { }, DepositRelayState::RelayDeposits { ref mut future, block } => { let _ = try_ready!(future.poll()); + info!("deposit relay completed"); DepositRelayState::Yield(Some(block)) }, DepositRelayState::Yield(ref mut block) => match block.take() { diff --git a/bridge/src/bridge/withdraw_relay.rs b/bridge/src/bridge/withdraw_relay.rs index 1fe36f3..4bd269f 100644 --- a/bridge/src/bridge/withdraw_relay.rs +++ b/bridge/src/bridge/withdraw_relay.rs @@ -37,7 +37,7 @@ fn signatures_payload(foreign: &foreign::ForeignBridge, required_signatures: u32 data: log.data.0, }; let collected_signatures = foreign.events().collected_signatures().parse_log(raw_log)?; - if collected_signatures.authority != my_address.0 { + if collected_signatures.authority_responsible_for_relay != my_address.0 { info!("bridge not responsible for relaying transaction to home. tx hash: {}", log.transaction_hash.unwrap()); // this authority is not responsible for relaying this transaction. // someone else will relay this transaction to home. diff --git a/contracts/bridge.sol b/contracts/bridge.sol index f01334f..bb743ba 100644 --- a/contracts/bridge.sol +++ b/contracts/bridge.sol @@ -259,57 +259,127 @@ contract HomeBridge { contract ForeignBridge { + // following is the part of ForeignBridge that implements an ERC20 token. + // ERC20 spec: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md + + uint public totalSupply; + + string public name = "ForeignBridge"; + + /// maps addresses to their token balances + mapping (address => uint) public balances; + + // owner of account approves the transfer of an amount by another account + mapping(address => mapping (address => uint)) allowed; + + /// Event created on money transfer + event Transfer(address indexed from, address indexed to, uint tokens); + + // returns the ERC20 token balance of the given address + function balanceOf(address tokenOwner) public view returns (uint) { + return balances[tokenOwner]; + } + + /// Transfer `value` to `recipient` on this `foreign` chain. + /// + /// does not affect `home` chain. does not do a relay. + /// as specificed in ERC20 this doesn't fail if tokens == 0. + function transfer(address recipient, uint tokens) public returns (bool) { + require(balances[msg.sender] >= tokens); + // fails if there is an overflow + require(balances[recipient] + tokens >= balances[recipient]); + + balances[msg.sender] -= tokens; + balances[recipient] += tokens; + Transfer(msg.sender, recipient, tokens); + return true; + } + + // following is the part of ForeignBridge that is concerned + // with the part of the ERC20 standard responsible for giving others spending rights + // and spending others tokens + + // created when `approve` is executed to mark that + // `tokenOwner` has approved `spender` to spend `tokens` of his tokens + event Approval(address indexed tokenOwner, address indexed spender, uint tokens); + + // allow `spender` to withdraw from your account, multiple times, up to the `tokens` amount. + // calling this function repeatedly overwrites the current allowance. + function approve(address spender, uint tokens) public returns (bool) { + allowed[msg.sender][spender] = tokens; + Approval(msg.sender, spender, tokens); + return true; + } + + // returns how much `spender` is allowed to spend of `owner`s tokens + function allowance(address owner, address spender) public view returns (uint256) { + return allowed[owner][spender]; + } + + function transferFrom(address from, address to, uint tokens) public returns (bool) { + // `from` has enough tokens + require(balances[from] >= tokens); + // `sender` is allowed to move `tokens` from `from` + require(allowed[from][msg.sender] >= tokens); + // fails if there is an overflow + require(balances[to] + tokens >= balances[to]); + + balances[to] += tokens; + balances[from] -= tokens; + allowed[from][msg.sender] -= tokens; + + Transfer(from, to, tokens); + return true; + } + + // following is the part of ForeignBridge that is + // no longer part of ERC20 and is concerned with + // with moving tokens from and to HomeBridge + struct SignaturesCollection { /// Signed message. bytes message; /// Authorities who signed the message. address[] signed; - /// Signaturs + /// Signatures bytes[] signatures; } /// Number of authorities signatures required to withdraw the money. /// - /// Must be lesser than number of authorities. + /// Must be less than number of authorities. uint public requiredSignatures; /// Contract authorities. address[] public authorities; - /// Ether balances - mapping (address => uint) public balances; - /// Pending deposits and authorities who confirmed them mapping (bytes32 => address[]) deposits; /// Pending signatures and authorities who confirmed them mapping (bytes32 => SignaturesCollection) signatures; - /// Event created on money deposit. + /// triggered when relay of deposit from HomeBridge is complete event Deposit(address recipient, uint value); /// Event created on money withdraw. event Withdraw(address recipient, uint value); - /// Event created on money transfer - event Transfer(address from, address to, uint value); - /// Collected signatures which should be relayed to home chain. - event CollectedSignatures(address authority, bytes32 messageHash); + event CollectedSignatures(address authorityResponsibleForRelay, bytes32 messageHash); - /// Constructor. function ForeignBridge( - uint requiredSignaturesParam, - address[] authoritiesParam + uint _requiredSignatures, + address[] _authorities ) public { - require(requiredSignaturesParam != 0); - require(requiredSignaturesParam <= authoritiesParam.length); - requiredSignatures = requiredSignaturesParam; - authorities = authoritiesParam; + require(_requiredSignatures != 0); + require(_requiredSignatures <= _authorities.length); + requiredSignatures = _requiredSignatures; + authorities = _authorities; } - /// Multisig authority validation + /// require that sender is an authority modifier onlyAuthority() { require(Helpers.addressArrayContains(authorities, msg.sender)); _; @@ -321,16 +391,22 @@ contract ForeignBridge { /// deposit value (uint) /// mainnet transaction hash (bytes32) // to avoid transaction duplication function deposit(address recipient, uint value, bytes32 transactionHash) public onlyAuthority() { - // Protection from misbehaing authority + // Protection from misbehaving authority var hash = keccak256(recipient, value, transactionHash); - // Duplicated deposits + // don't allow authority to confirm deposit twice require(!Helpers.addressArrayContains(deposits[hash], msg.sender)); deposits[hash].push(msg.sender); - // TODO: this may cause troubles if requriedSignatures len is changed + // TODO: this may cause troubles if requiredSignatures len is changed if (deposits[hash].length == requiredSignatures) { balances[recipient] += value; + // mints tokens + totalSupply += value; + // ERC20 specifies: a token contract which creates new tokens + // SHOULD trigger a Transfer event with the _from address + // set to 0x0 when tokens are created. + Transfer(0x0, recipient, value); Deposit(recipient, value); } } @@ -346,26 +422,19 @@ contract ForeignBridge { /// which transfers `value - relayCost` to `recipient` completing the transfer. function transferHomeViaRelay(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]); + // don't allow 0 value transfers to home + require(value > 0); balances[msg.sender] -= value; + // burns tokens + totalSupply -= value; + // in line with the transfer event from `0x0` on token creation + // recommended by ERC20 (see implementation of `deposit` above) + // we trigger a Transfer event to `0x0` on token destruction + Transfer(msg.sender, 0x0, 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. diff --git a/integration-tests/bridge_config.toml b/integration-tests/bridge_config.toml index c1d2979..511aa8a 100644 --- a/integration-tests/bridge_config.toml +++ b/integration-tests/bridge_config.toml @@ -23,8 +23,8 @@ accounts = [ required_signatures = 1 [transactions] -home_deploy = { gas = 1000000 } +home_deploy = { gas = 3000000 } foreign_deploy = { gas = 3000000 } -deposit_relay = { gas = 100000 } +deposit_relay = { gas = 3000000 } withdraw_relay = { gas = 3000000 } withdraw_confirm = { gas = 3000000 } diff --git a/integration-tests/tests/basic_deposit_then_withdraw.rs b/integration-tests/tests/basic_deposit_then_withdraw.rs index df51f4e..773894e 100644 --- a/integration-tests/tests/basic_deposit_then_withdraw.rs +++ b/integration-tests/tests/basic_deposit_then_withdraw.rs @@ -60,6 +60,10 @@ fn parity_foreign_command() -> Command { command } +fn address_from_str(string: &'static str) -> web3::types::Address { + web3::types::Address::from(&Address::from(string).0[..]) +} + #[test] fn test_basic_deposit_then_withdraw() { if Path::new(TMP_PATH).exists() { @@ -74,7 +78,7 @@ fn test_basic_deposit_then_withdraw() { .arg("build") .status() .expect("failed to build bridge cli") - .success()); + .success()); // start a parity node that represents the home chain let mut parity_home = parity_home_command() @@ -89,6 +93,13 @@ fn test_basic_deposit_then_withdraw() { // give the clients time to start up thread::sleep(Duration::from_millis(3000)); + // A address containing a lot of tokens (0x00a329c0648769a73afac7f9381e08fb43dbea72) should be + // automatically added with a password being an empty string. + // source: https://paritytech.github.io/wiki/Private-development-chain.html + let user_address = "0x00a329c0648769a73afac7f9381e08fb43dbea72"; + + let authority_address = "0x00bd138abd70e2f00903268f3db08f2d25677c9e"; + // create authority account on home let exit_status = Command::new("curl") .arg("--data").arg(r#"{"jsonrpc":"2.0","method":"parity_newAccountFromPhrase","params":["node0", ""],"id":0}"#) @@ -98,6 +109,7 @@ fn test_basic_deposit_then_withdraw() { .status() .expect("failed to create authority account on home"); assert!(exit_status.success()); + // TODO [snd] assert that created address matches authority_address // create authority account on foreign let exit_status = Command::new("curl") @@ -108,6 +120,7 @@ fn test_basic_deposit_then_withdraw() { .status() .expect("failed to create/unlock authority account on foreign"); assert!(exit_status.success()); + // TODO [snd] assert that created address matches authority_address // give the operations time to complete thread::sleep(Duration::from_millis(5000)); @@ -121,14 +134,14 @@ fn test_basic_deposit_then_withdraw() { // start a parity node that represents the home chain with accounts unlocked let mut parity_home = parity_home_command() - .arg("--unlock").arg("0x00a329c0648769a73afac7f9381e08fb43dbea72,0x00bd138abd70e2f00903268f3db08f2d25677c9e") + .arg("--unlock").arg(format!("{},{}", user_address, authority_address)) .arg("--password").arg("password.txt") .spawn() .expect("failed to spawn parity home node"); // start a parity node that represents the foreign chain with accounts unlocked let mut parity_foreign = parity_foreign_command() - .arg("--unlock").arg("0x00a329c0648769a73afac7f9381e08fb43dbea72,0x00bd138abd70e2f00903268f3db08f2d25677c9e") + .arg("--unlock").arg(format!("{},{}", user_address, authority_address)) .arg("--password").arg("password.txt") .spawn() .expect("failed to spawn parity foreign node"); @@ -149,15 +162,18 @@ fn test_basic_deposit_then_withdraw() { // give the bridge time to start up and deploy the contracts thread::sleep(Duration::from_millis(10000)); - println!("\ndeposit ether into HomeBridge\n"); + let home_contract_address = "0xebd3944af37ccc6b67ff61239ac4fef229c8f69f"; + let foreign_contract_address = "0xebd3944af37ccc6b67ff61239ac4fef229c8f69f"; - // user deposits into HomeBridge + println!("\nuser deposits ether into HomeBridge\n"); + + // TODO [snd] use rpc client here instead of curl let exit_status = Command::new("curl") - .arg("--data").arg(r#"{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[{ - "from": "0x00a329c0648769a73afac7f9381e08fb43dbea72", - "to": "0xebd3944af37ccc6b67ff61239ac4fef229c8f69f", + .arg("--data").arg(format!(r#"{{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[{{ + "from": "{}", + "to": "{}", "value": "0x186a0" - }],"id":0}"#) + }}],"id":0}}"#, user_address, home_contract_address)) .arg("-H").arg("Content-Type: application/json") .arg("-X").arg("POST") .arg("localhost:8550") @@ -168,7 +184,7 @@ fn test_basic_deposit_then_withdraw() { println!("\ndeposit into home sent. give it plenty of time to get mined and relayed\n"); thread::sleep(Duration::from_millis(10000)); - // connect for foreign and home via IPC + // connect to foreign and home via IPC let mut event_loop = Core::new().unwrap(); let foreign_transport = Ipc::with_event_loop("foreign.ipc", &event_loop.handle()) .expect("failed to connect to foreign.ipc"); @@ -178,12 +194,30 @@ fn test_basic_deposit_then_withdraw() { .expect("failed to connect to home.ipc"); let home_eth = web3::api::Eth::new(home_transport); - // balance on ForeignBridge should have increased - let balance_payload = foreign.functions().balances().input(Address::from("0x00a329c0648769a73afac7f9381e08fb43dbea72")); + // totalSupply on ForeignBridge should have increased + let total_supply_payload = foreign.functions().total_supply().input(); let future = foreign_eth.call(web3::types::CallRequest{ from: None, - to: web3::types::Address::from(&Address::from("0xebd3944af37ccc6b67ff61239ac4fef229c8f69f").0[..]), + to: address_from_str(foreign_contract_address), + gas: None, + gas_price: None, + value: None, + data: Some(web3::types::Bytes(total_supply_payload)), + }, None); + + let response = event_loop.run(future).unwrap(); + assert_eq!( + U256::from(response.0.as_slice()), + U256::from(100000), + "totalSupply on ForeignBridge should have increased"); + + // balance on ForeignBridge should have increased + let balance_payload = foreign.functions().balance_of().input(Address::from(user_address)); + + let future = foreign_eth.call(web3::types::CallRequest{ + from: None, + to: address_from_str(foreign_contract_address), gas: None, gas_price: None, value: None, @@ -193,21 +227,21 @@ fn test_basic_deposit_then_withdraw() { let response = event_loop.run(future).unwrap(); let balance = U256::from(response.0.as_slice()); assert_eq!( - balance, - U256::from(100000), - "balance on ForeignBridge should have increased"); + balance, + U256::from(100000), + "balance on ForeignBridge should have increased"); println!("\nconfirmed that deposit reached foreign\n"); - // withdraw + println!("\nuser executes ForeignBridge.transferHomeViaRelay\n"); let transfer_payload = foreign.functions() .transfer_home_via_relay() .input( - Address::from("0x00aa39d30f0d20ff03a22ccfc30b7efbfca597c2"), + Address::from(user_address), U256::from(100000)); let future = foreign_eth.send_transaction(web3::types::TransactionRequest{ - from: web3::types::Address::from(&Address::from("0x00a329c0648769a73afac7f9381e08fb43dbea72").0[..]), - to: Some(web3::types::Address::from(&Address::from("0xebd3944af37ccc6b67ff61239ac4fef229c8f69f").0[..])), + from: address_from_str(user_address), + to: Some(address_from_str(foreign_contract_address)), gas: None, gas_price: None, value: None, @@ -221,7 +255,7 @@ fn test_basic_deposit_then_withdraw() { thread::sleep(Duration::from_millis(10000)); // test that withdraw completed - let future = home_eth.balance(web3::types::Address::from(&Address::from("0x00aa39d30f0d20ff03a22ccfc30b7efbfca597c2").0[..]), None); + let future = home_eth.balance(address_from_str(user_address), None); println!("waiting for future"); let balance = event_loop.run(future).unwrap(); assert!(balance > web3::types::U256::from(0)); diff --git a/truffle/.node-xmlhttprequest-content-37792 b/truffle/.node-xmlhttprequest-content-37792 new file mode 100644 index 0000000..6551190 --- /dev/null +++ b/truffle/.node-xmlhttprequest-content-37792 @@ -0,0 +1 @@ +{"err":null,"data":{"statusCode":200,"headers":{"access-control-allow-headers":"Origin, X-Requested-With, Content-Type, Accept","access-control-allow-origin":"*","access-control-allow-methods":"*","content-type":"application/json","date":"Fri, 26 Jan 2018 09:06:17 GMT","connection":"close","transfer-encoding":"chunked"},"text":"{\"id\":723,\"jsonrpc\":\"2.0\",\"result\":\"0x00000000000000056bc75e2d63100000\"}"}} \ No newline at end of file diff --git a/truffle/.node-xmlhttprequest-sync-37792 b/truffle/.node-xmlhttprequest-sync-37792 new file mode 100644 index 0000000..e69de29 diff --git a/truffle/test/foreign-erc20.js b/truffle/test/foreign-erc20.js new file mode 100644 index 0000000..0691b7c --- /dev/null +++ b/truffle/test/foreign-erc20.js @@ -0,0 +1,253 @@ +var ForeignBridge = artifacts.require("ForeignBridge"); +var helpers = require("./helpers/helpers"); + +contract('ForeignBridge', function(accounts) { + it("totalSupply", function() { + var contract; + var requiredSignatures = 1; + var authorities = [accounts[0], accounts[1]]; + var owner = accounts[2]; + var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408"; + var value = web3.toWei(3, "ether"); + + return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) { + contract = instance; + + return contract.totalSupply(); + }).then(function(result) { + assert.equal(0, result, "initial supply should be 0"); + + return contract.deposit(owner, value, hash, {from: authorities[0]}); + }).then(function(result) { + + return contract.totalSupply(); + }).then(function(result) { + console.log(result); + assert(result.equals(value), "deposit should increase supply"); + + return contract.transferHomeViaRelay(owner, value, {from: owner}); + }).then(function() { + + return contract.totalSupply(); + }).then(function(result) { + assert.equal(0, result, "home transfer should decrease supply"); + }) + }) + + it("should be able to approve others to spend tokens in their name", function() { + var contract; + var requiredSignatures = 1; + var authorities = [accounts[0], accounts[1]]; + var owner = accounts[2]; + var spender = accounts[3]; + var receiver = accounts[4]; + var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408"; + + return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) { + contract = instance; + + // deposit something so we can transfer it + return contract.deposit(owner, web3.toWei(3, "ether"), hash, {from: authorities[0]}); + }).then(function(result) { + + return contract.allowance(owner, spender); + }).then(function(result) { + assert.equal(0, result, "initial allowance should be 0"); + + return contract.transferFrom(owner, receiver, web3.toWei(1, "ether"), {from: spender}); + }).then(function(result) { + assert(false, "transfer without allowance should fail"); + + // transfer 0 without allowance should work + return contract.transferFrom(owner, receiver, 0, {from: spender}); + }).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(owner, result.logs[0].args.from); + assert.equal(receiver, result.logs[0].args.to); + assert.equal(0, result.logs[0].args.tokens); + + }, function(err) { + return contract.approve(spender, web3.toWei(4, "ether"), {from: owner}); + }).then(function(result) { + assert.equal(1, result.logs.length, "Exactly one event should be created"); + assert.equal("Approval", result.logs[0].event, "Event name should be Approval"); + assert.equal(owner, result.logs[0].args.tokenOwner); + assert.equal(spender, result.logs[0].args.spender); + assert.equal(web3.toWei(4, "ether"), result.logs[0].args.tokens); + + return contract.allowance(owner, spender); + }).then(function(result) { + assert.equal(web3.toWei(4, "ether"), result, "approval should set allowance"); + + return contract.transferFrom(owner, receiver, web3.toWei(4, "ether"), {from: spender}); + }).then(function(result) { + assert(false, "transferring more than balance should fail"); + }, function(err) { + return contract.approve(spender, web3.toWei(2, "ether"), {from: owner}); + }).then(function(result) { + assert.equal(1, result.logs.length, "Exactly one event should be created"); + assert.equal("Approval", result.logs[0].event, "Event name should be Approval"); + assert.equal(owner, result.logs[0].args.tokenOwner); + assert.equal(spender, result.logs[0].args.spender); + assert.equal(web3.toWei(2, "ether"), result.logs[0].args.tokens); + + return contract.allowance(owner, spender); + }).then(function(result) { + assert.equal(web3.toWei(2, "ether"), result, "approval should update allowance"); + + return contract.transferFrom(owner, receiver, web3.toWei(2, "ether") + 2, {from: spender}); + }).then(function(result) { + assert(false, "transferring more than allowance should fail"); + }, function(err) { + return contract.transferFrom(owner, receiver, web3.toWei(2, "ether"), {from: spender}); + }).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(owner, result.logs[0].args.from); + assert.equal(receiver, result.logs[0].args.to); + assert.equal(web3.toWei(2, "ether"), result.logs[0].args.tokens); + + return contract.balanceOf(owner); + }).then(function(result) { + assert.equal(web3.toWei(1, "ether"), result, "transferring should reduce owners balance"); + + return contract.balanceOf(receiver); + }).then(function(result) { + assert.equal(web3.toWei(2, "ether"), result, "transferring should increase receivers balance"); + + return contract.balanceOf(spender); + }).then(function(result) { + assert.equal(0, result, "transferring should not modify spenders balance"); + + return contract.allowance(owner, spender); + }).then(function(result) { + assert.equal(0, result, "transferring whole allowance should set allowance to 0"); + }) + }) + + it("should allow user to transfer value locally", function() { + var meta; + var requiredSignatures = 1; + var authorities = [accounts[0], accounts[1]]; + 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; + // top up balance so we can transfer + return meta.deposit(userAccount, user1InitialValue, hash, { from: authorities[0] }); + }).then(function(result) { + return meta.transfer(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(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.tokens, "Event tokens should match transaction value"); + return Promise.all([ + meta.balances.call(userAccount), + meta.balances.call(userAccount2) + ]) + }).then(function(result) { + assert.equal(web3.toWei(2, "ether"), result[0]); + assert.equal(transferedValue, result[1]); + }) + }) + + it("should not allow user to transfer value they don't have", 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(4, "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.transfer(recipientAccount, transferedValue, { from: userAccount }); + }).then(function(result) { + assert(false, "transfer should fail"); + }, function(err) { + }) + }) + + it("should allow transfer of 0 value according to ERC20", 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 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.transfer(recipientAccount, 0, { 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(userAccount, result.logs[0].args.from, "Event from should be transaction sender"); + assert.equal(recipientAccount, result.logs[0].args.to, "Event from should be transaction recipient"); + assert.equal(0, result.logs[0].args.tokens, "Event tokens should match transaction value"); + return Promise.all([ + meta.balances.call(userAccount), + meta.balances.call(recipientAccount) + ]) + }).then(function(result) { + assert.equal(userValue, result[0]); + assert.equal(0, result[1]); + }) + }) + + it("transfer that results in overflow should fail", function() { + var meta; + var requiredSignatures = 1; + var authorities = [accounts[0], accounts[1]]; + var userAccount = accounts[2]; + var recipientAccount = accounts[3]; + var maxValue = web3.toWei("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "wei"); + var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408"; + return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) { + meta = instance; + return meta.deposit(recipientAccount, maxValue, hash, { from: authorities[0] }); + }).then(function(result) { + return meta.deposit(userAccount, 1, hash, { from: authorities[0] }); + }).then(function(result) { + return meta.transfer(recipientAccount, 1, { from: userAccount }); + }).then(function(result) { + assert(false, "transfer should fail"); + }, function(err) { + }) + }) + + it("transferFrom that results in overflow should fail", function() { + var meta; + var requiredSignatures = 1; + var authorities = [accounts[0], accounts[1]]; + var userAccount = accounts[2]; + var spenderAccount = accounts[3]; + var recipientAccount = accounts[4]; + var maxValue = web3.toWei("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "wei"); + var hash = "0xe55bb43c36cdf79e23b4adc149cdded921f0d482e613c50c6540977c213bc408"; + return ForeignBridge.new(requiredSignatures, authorities).then(function(instance) { + meta = instance; + return meta.deposit(recipientAccount, maxValue, hash, { from: authorities[0] }); + }).then(function(result) { + return meta.deposit(userAccount, 1, hash, { from: authorities[0] }); + }).then(function(result) { + return meta.approve(spenderAccount, 1, {from: userAccount}); + }).then(function(result) { + return meta.transferFrom(userAccount, recipientAccount, 1, { from: spenderAccount }); + }).then(function(result) { + assert(false, "transfer should fail"); + }, function(err) { + }) + }) +}) diff --git a/truffle/test/foreign.js b/truffle/test/foreign.js index 2e51207..4d5792e 100644 --- a/truffle/test/foreign.js +++ b/truffle/test/foreign.js @@ -48,10 +48,17 @@ contract('ForeignBridge', function(accounts) { meta = instance; 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(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"); + assert.equal(2, result.logs.length) + + assert.equal("Transfer", result.logs[0].event); + assert.equal("0x0000000000000000000000000000000000000000", result.logs[0].args.from); + assert.equal(userAccount, result.logs[0].args.to); + assert.equal(value, result.logs[0].args.tokens); + + assert.equal("Deposit", result.logs[1].event); + assert.equal(userAccount, result.logs[1].args.recipient); + assert.equal(value, result.logs[1].args.value); + return meta.balances.call(userAccount); }).then(function(result) { assert.equal(value, result, "Contract balance should change"); @@ -76,10 +83,16 @@ contract('ForeignBridge', function(accounts) { assert.equal(web3.toWei(0, "ether"), result, "Contract balance should not change yet"); 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(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"); + assert.equal(2, result.logs.length) + + assert.equal("Transfer", result.logs[0].event); + assert.equal("0x0000000000000000000000000000000000000000", result.logs[0].args.from); + assert.equal(userAccount, result.logs[0].args.to); + assert.equal(value, result.logs[0].args.tokens); + + assert.equal("Deposit", result.logs[1].event, "Event name should be Deposit"); + assert.equal(userAccount, result.logs[1].args.recipient, "Event recipient should be transaction sender"); + assert.equal(value, result.logs[1].args.value, "Event value should match deposited ether"); return meta.balances.call(userAccount); }).then(function(result) { assert.equal(value, result, "Contract balance should change"); @@ -141,48 +154,24 @@ contract('ForeignBridge', function(accounts) { assert.equal(0, result.logs.length, "Misbehaving authority should be ignored"); 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(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"); + assert.equal(2, result.logs.length) + + assert.equal("Transfer", result.logs[0].event); + assert.equal("0x0000000000000000000000000000000000000000", result.logs[0].args.from); + assert.equal(userAccount, result.logs[0].args.to); + assert.equal(value, result.logs[0].args.tokens); + + assert.equal("Deposit", result.logs[1].event, "Event name should be Deposit"); + assert.equal(userAccount, result.logs[1].args.recipient, "Event recipient should be transaction sender"); + assert.equal(value, result.logs[1].args.value, "Event value should match transaction value"); + return meta.balances.call(userAccount); }).then(function(result) { assert.equal(value, result, "Contract balance should change"); }) }) - it("should allow user to transfer value locally", function() { - var meta; - var requiredSignatures = 1; - var authorities = [accounts[0], accounts[1]]; - 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; - // top up balance so we can transfer - return meta.deposit(userAccount, user1InitialValue, hash, { from: authorities[0] }); - }).then(function(result) { - 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(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(userAccount), - meta.balances.call(userAccount2) - ]) - }).then(function(result) { - assert.equal(web3.toWei(2, "ether"), result[0]); - assert.equal(transferedValue, result[1]); - }) - }) - - it("should not allow user to transfer value they don't have either locally or to home", function() { + it("should not allow user to transfer value they don't have to home", function() { var meta; var requiredSignatures = 1; var authorities = [accounts[0], accounts[1]]; @@ -195,10 +184,6 @@ contract('ForeignBridge', function(accounts) { 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"); @@ -206,7 +191,7 @@ contract('ForeignBridge', function(accounts) { }) }) - it("should fail to transfer 0 value both locally and to home", function() { + it("should fail to transfer 0 value to home", function() { var meta; var requiredSignatures = 1; var authorities = [accounts[0], accounts[1]]; @@ -219,34 +204,6 @@ contract('ForeignBridge', function(accounts) { 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"); @@ -270,10 +227,17 @@ contract('ForeignBridge', function(accounts) { }).then(function(result) { 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(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"); + assert.equal(2, result.logs.length) + + assert.equal("Transfer", result.logs[0].event); + assert.equal(userAccount, result.logs[0].args.from); + assert.equal("0x0000000000000000000000000000000000000000", result.logs[0].args.to); + assert.equal(value2, result.logs[0].args.tokens); + + assert.equal("Withdraw", result.logs[1].event, "Event name should be Withdraw"); + assert.equal(userAccount2, result.logs[1].args.recipient, "Event recipient should be equal to transaction recipient"); + assert.equal(value2, result.logs[1].args.value, "Event value should match transaction value"); + return Promise.all([ meta.balances.call(userAccount), meta.balances.call(userAccount2) @@ -299,7 +263,7 @@ contract('ForeignBridge', function(accounts) { }).then(function(result) { assert.equal(1, result.logs.length, "Exactly one event should be created"); assert.equal("CollectedSignatures", result.logs[0].event, "Event name should be CollectedSignatures"); - assert.equal(authorities[0], result.logs[0].args.authority, "Event authority should be equal to transaction sender"); + assert.equal(authorities[0], result.logs[0].args.authorityResponsibleForRelay, "Event authority should be equal to transaction sender"); return Promise.all([ meta.signature.call(result.logs[0].args.messageHash, 0), meta.message(result.logs[0].args.messageHash), @@ -356,7 +320,7 @@ contract('ForeignBridge', function(accounts) { }).then(function(result) { assert.equal(1, result.logs.length, "Exactly one event should be created"); assert.equal("CollectedSignatures", result.logs[0].event, "Event name should be CollectedSignatures"); - assert.equal(authorities[0], result.logs[0].args.authority, "Event authority should be equal to transaction sender"); + assert.equal(authorities[0], result.logs[0].args.authorityResponsibleForRelay, "Event authority should be equal to transaction sender"); return Promise.all([ meta.signature.call(result.logs[0].args.messageHash, 0), meta.signature.call(result.logs[0].args.messageHash, 1), @@ -370,7 +334,7 @@ contract('ForeignBridge', function(accounts) { }).then(function(result) { assert.equal(1, result.logs.length, "Exactly one event should be created"); assert.equal("CollectedSignatures", result.logs[0].event, "Event name should be CollectedSignatures"); - assert.equal(authorities[1], result.logs[0].args.authority, "Event authority should be equal to transaction sender"); + assert.equal(authorities[1], result.logs[0].args.authorityResponsibleForRelay, "Event authority should be equal to transaction sender"); return Promise.all([ meta.signature.call(result.logs[0].args.messageHash, 0), meta.signature.call(result.logs[0].args.messageHash, 1),