From f2240f5ec3b8d5c0c6f7130ed6e6cef4a21f88f0 Mon Sep 17 00:00:00 2001 From: Mark Toda Date: Wed, 24 Jun 2020 12:10:05 -0700 Subject: [PATCH] Add Support for CELO, RSK and ETC This commit adds support for non-ETH chains with concrete implementations for CELO, RSK, and ETC. The main change for these chains is that the operation hash prefix must be specific to the chain, to avoid cross-chain replay attacks. For example, on ETH chain it is the string "ETHER", on RSK chain it is the string "RSK" etc. In addition, this commit extends the test suite to cover the wallet implementations, and adds a test to ensure that signatures with the wrong operatin hash prefix fail. Ticket: BG-22389 --- contracts/coins/CeloWalletSimple.sol | 312 ++++++ contracts/coins/EtcWalletSimple.sol | 312 ++++++ contracts/coins/RskWalletSimple.sol | 312 ++++++ mocha-test/compile.js | 8 +- test/helpers.js | 8 +- test/walletsimple.js | 1492 +++++++++++++------------- 6 files changed, 1718 insertions(+), 726 deletions(-) create mode 100644 contracts/coins/CeloWalletSimple.sol create mode 100644 contracts/coins/EtcWalletSimple.sol create mode 100644 contracts/coins/RskWalletSimple.sol diff --git a/contracts/coins/CeloWalletSimple.sol b/contracts/coins/CeloWalletSimple.sol new file mode 100644 index 0000000..9a0e296 --- /dev/null +++ b/contracts/coins/CeloWalletSimple.sol @@ -0,0 +1,312 @@ +pragma solidity ^0.4.18; +import "../Forwarder.sol"; +import "../ERC20Interface.sol"; +/** + * + * WalletSimple + * ============ + * + * Basic multi-signer wallet designed for use in a co-signing environment where 2 signatures are required to move funds. + * Typically used in a 2-of-3 signing configuration. Uses ecrecover to allow for 2 signatures in a single transaction. + * + * The first signature is created on the operation hash (see Data Formats) and passed to sendMultiSig/sendMultiSigToken + * The signer is determined by verifyMultiSig(). + * + * The second signature is created by the submitter of the transaction and determined by msg.signer. + * + * Data Formats + * ============ + * + * The signature is created with ethereumjs-util.ecsign(operationHash). + * Like the eth_sign RPC call, it packs the values as a 65-byte array of [r, s, v]. + * Unlike eth_sign, the message is not prefixed. + * + * The operationHash the result of keccak256(prefix, toAddress, value, data, expireTime). + * For ether transactions, `prefix` is "ETHER". + * For token transaction, `prefix` is "ERC20" and `data` is the tokenContractAddress. + * + * + */ +contract CeloWalletSimple { + // Events + event Deposited(address from, uint value, bytes data); + event SafeModeActivated(address msgSender); + event Transacted( + address msgSender, // Address of the sender of the message initiating the transaction + address otherSigner, // Address of the signer (second signature) used to initiate the transaction + bytes32 operation, // Operation hash (see Data Formats) + address toAddress, // The address the transaction was sent to + uint value, // Amount of Wei sent to the address + bytes data // Data sent when invoking the transaction + ); + + // Public fields + address[] public signers; // The addresses that can co-sign transactions on the wallet + bool public safeMode = false; // When active, wallet may only send to signer addresses + + // Internal fields + uint constant SEQUENCE_ID_WINDOW_SIZE = 10; + uint[10] recentSequenceIds; + + /** + * Set up a simple multi-sig wallet by specifying the signers allowed to be used on this wallet. + * 2 signers will be required to send a transaction from this wallet. + * Note: The sender is NOT automatically added to the list of signers. + * Signers CANNOT be changed once they are set + * + * @param allowedSigners An array of signers on the wallet + */ + function CeloWalletSimple(address[] allowedSigners) public { + if (allowedSigners.length != 3) { + // Invalid number of signers + revert(); + } + signers = allowedSigners; + } + + /** + * Determine if an address is a signer on this wallet + * @param signer address to check + * returns boolean indicating whether address is signer or not + */ + function isSigner(address signer) public view returns (bool) { + // Iterate through all signers on the wallet and + for (uint i = 0; i < signers.length; i++) { + if (signers[i] == signer) { + return true; + } + } + return false; + } + + /** + * Modifier that will execute internal code block only if the sender is an authorized signer on this wallet + */ + modifier onlySigner { + if (!isSigner(msg.sender)) { + revert(); + } + _; + } + + /** + * Gets called when a transaction is received without calling a method + */ + function() public payable { + if (msg.value > 0) { + // Fire deposited event if we are receiving funds + Deposited(msg.sender, msg.value, msg.data); + } + } + + /** + * Create a new contract (and also address) that forwards funds to this contract + * returns address of newly created forwarder address + */ + function createForwarder() public returns (address) { + return new Forwarder(); + } + + /** + * Execute a multi-signature transaction from this wallet using 2 signers: one from msg.sender and the other from ecrecover. + * Sequence IDs are numbers starting from 1. They are used to prevent replay attacks and may not be repeated. + * + * @param toAddress the destination address to send an outgoing transaction + * @param value the amount in Wei to be sent + * @param data the data to send to the toAddress when invoking the transaction + * @param expireTime the number of seconds since 1970 for which this transaction is valid + * @param sequenceId the unique sequence id obtainable from getNextSequenceId + * @param signature see Data Formats + */ + function sendMultiSig( + address toAddress, + uint value, + bytes data, + uint expireTime, + uint sequenceId, + bytes signature + ) public onlySigner { + // Verify the other signer + var operationHash = keccak256("CELO", toAddress, value, data, expireTime, sequenceId); + + var otherSigner = verifyMultiSig(toAddress, operationHash, signature, expireTime, sequenceId); + + // Success, send the transaction + if (!(toAddress.call.value(value)(data))) { + // Failed executing transaction + revert(); + } + Transacted(msg.sender, otherSigner, operationHash, toAddress, value, data); + } + + /** + * Execute a multi-signature token transfer from this wallet using 2 signers: one from msg.sender and the other from ecrecover. + * Sequence IDs are numbers starting from 1. They are used to prevent replay attacks and may not be repeated. + * + * @param toAddress the destination address to send an outgoing transaction + * @param value the amount in tokens to be sent + * @param tokenContractAddress the address of the erc20 token contract + * @param expireTime the number of seconds since 1970 for which this transaction is valid + * @param sequenceId the unique sequence id obtainable from getNextSequenceId + * @param signature see Data Formats + */ + function sendMultiSigToken( + address toAddress, + uint value, + address tokenContractAddress, + uint expireTime, + uint sequenceId, + bytes signature + ) public onlySigner { + // Verify the other signer + var operationHash = keccak256("CELO-ERC20", toAddress, value, tokenContractAddress, expireTime, sequenceId); + + verifyMultiSig(toAddress, operationHash, signature, expireTime, sequenceId); + + ERC20Interface instance = ERC20Interface(tokenContractAddress); + if (!instance.transfer(toAddress, value)) { + revert(); + } + } + + /** + * Execute a token flush from one of the forwarder addresses. This transfer needs only a single signature and can be done by any signer + * + * @param forwarderAddress the address of the forwarder address to flush the tokens from + * @param tokenContractAddress the address of the erc20 token contract + */ + function flushForwarderTokens( + address forwarderAddress, + address tokenContractAddress + ) public onlySigner { + Forwarder forwarder = Forwarder(forwarderAddress); + forwarder.flushTokens(tokenContractAddress); + } + + /** + * Do common multisig verification for both eth sends and erc20token transfers + * + * @param toAddress the destination address to send an outgoing transaction + * @param operationHash see Data Formats + * @param signature see Data Formats + * @param expireTime the number of seconds since 1970 for which this transaction is valid + * @param sequenceId the unique sequence id obtainable from getNextSequenceId + * returns address that has created the signature + */ + function verifyMultiSig( + address toAddress, + bytes32 operationHash, + bytes signature, + uint expireTime, + uint sequenceId + ) private returns (address) { + + var otherSigner = recoverAddressFromSignature(operationHash, signature); + + // Verify if we are in safe mode. In safe mode, the wallet can only send to signers + if (safeMode && !isSigner(toAddress)) { + // We are in safe mode and the toAddress is not a signer. Disallow! + revert(); + } + // Verify that the transaction has not expired + if (expireTime < block.timestamp) { + // Transaction expired + revert(); + } + + // Try to insert the sequence ID. Will revert if the sequence id was invalid + tryInsertSequenceId(sequenceId); + + if (!isSigner(otherSigner)) { + // Other signer not on this wallet or operation does not match arguments + revert(); + } + if (otherSigner == msg.sender) { + // Cannot approve own transaction + revert(); + } + + return otherSigner; + } + + /** + * Irrevocably puts contract into safe mode. When in this mode, transactions may only be sent to signing addresses. + */ + function activateSafeMode() public onlySigner { + safeMode = true; + SafeModeActivated(msg.sender); + } + + /** + * Gets signer's address using ecrecover + * @param operationHash see Data Formats + * @param signature see Data Formats + * returns address recovered from the signature + */ + function recoverAddressFromSignature( + bytes32 operationHash, + bytes signature + ) private pure returns (address) { + if (signature.length != 65) { + revert(); + } + // We need to unpack the signature, which is given as an array of 65 bytes (like eth.sign) + bytes32 r; + bytes32 s; + uint8 v; + assembly { + r := mload(add(signature, 32)) + s := mload(add(signature, 64)) + v := and(mload(add(signature, 65)), 255) + } + if (v < 27) { + v += 27; // Ethereum versions are 27 or 28 as opposed to 0 or 1 which is submitted by some signing libs + } + return ecrecover(operationHash, v, r, s); + } + + /** + * Verify that the sequence id has not been used before and inserts it. Throws if the sequence ID was not accepted. + * We collect a window of up to 10 recent sequence ids, and allow any sequence id that is not in the window and + * greater than the minimum element in the window. + * @param sequenceId to insert into array of stored ids + */ + function tryInsertSequenceId(uint sequenceId) private onlySigner { + // Keep a pointer to the lowest value element in the window + uint lowestValueIndex = 0; + for (uint i = 0; i < SEQUENCE_ID_WINDOW_SIZE; i++) { + if (recentSequenceIds[i] == sequenceId) { + // This sequence ID has been used before. Disallow! + revert(); + } + if (recentSequenceIds[i] < recentSequenceIds[lowestValueIndex]) { + lowestValueIndex = i; + } + } + if (sequenceId < recentSequenceIds[lowestValueIndex]) { + // The sequence ID being used is lower than the lowest value in the window + // so we cannot accept it as it may have been used before + revert(); + } + if (sequenceId > (recentSequenceIds[lowestValueIndex] + 10000)) { + // Block sequence IDs which are much higher than the lowest value + // This prevents people blocking the contract by using very large sequence IDs quickly + revert(); + } + recentSequenceIds[lowestValueIndex] = sequenceId; + } + + /** + * Gets the next available sequence ID for signing when using executeAndConfirm + * returns the sequenceId one higher than the highest currently stored + */ + function getNextSequenceId() public view returns (uint) { + uint highestSequenceId = 0; + for (uint i = 0; i < SEQUENCE_ID_WINDOW_SIZE; i++) { + if (recentSequenceIds[i] > highestSequenceId) { + highestSequenceId = recentSequenceIds[i]; + } + } + return highestSequenceId + 1; + } +} diff --git a/contracts/coins/EtcWalletSimple.sol b/contracts/coins/EtcWalletSimple.sol new file mode 100644 index 0000000..de24e0b --- /dev/null +++ b/contracts/coins/EtcWalletSimple.sol @@ -0,0 +1,312 @@ +pragma solidity ^0.4.18; +import "../Forwarder.sol"; +import "../ERC20Interface.sol"; +/** + * + * WalletSimple + * ============ + * + * Basic multi-signer wallet designed for use in a co-signing environment where 2 signatures are required to move funds. + * Typically used in a 2-of-3 signing configuration. Uses ecrecover to allow for 2 signatures in a single transaction. + * + * The first signature is created on the operation hash (see Data Formats) and passed to sendMultiSig/sendMultiSigToken + * The signer is determined by verifyMultiSig(). + * + * The second signature is created by the submitter of the transaction and determined by msg.signer. + * + * Data Formats + * ============ + * + * The signature is created with ethereumjs-util.ecsign(operationHash). + * Like the eth_sign RPC call, it packs the values as a 65-byte array of [r, s, v]. + * Unlike eth_sign, the message is not prefixed. + * + * The operationHash the result of keccak256(prefix, toAddress, value, data, expireTime). + * For ether transactions, `prefix` is "ETHER". + * For token transaction, `prefix` is "ERC20" and `data` is the tokenContractAddress. + * + * + */ +contract EtcWalletSimple { + // Events + event Deposited(address from, uint value, bytes data); + event SafeModeActivated(address msgSender); + event Transacted( + address msgSender, // Address of the sender of the message initiating the transaction + address otherSigner, // Address of the signer (second signature) used to initiate the transaction + bytes32 operation, // Operation hash (see Data Formats) + address toAddress, // The address the transaction was sent to + uint value, // Amount of Wei sent to the address + bytes data // Data sent when invoking the transaction + ); + + // Public fields + address[] public signers; // The addresses that can co-sign transactions on the wallet + bool public safeMode = false; // When active, wallet may only send to signer addresses + + // Internal fields + uint constant SEQUENCE_ID_WINDOW_SIZE = 10; + uint[10] recentSequenceIds; + + /** + * Set up a simple multi-sig wallet by specifying the signers allowed to be used on this wallet. + * 2 signers will be required to send a transaction from this wallet. + * Note: The sender is NOT automatically added to the list of signers. + * Signers CANNOT be changed once they are set + * + * @param allowedSigners An array of signers on the wallet + */ + function EtcWalletSimple(address[] allowedSigners) public { + if (allowedSigners.length != 3) { + // Invalid number of signers + revert(); + } + signers = allowedSigners; + } + + /** + * Determine if an address is a signer on this wallet + * @param signer address to check + * returns boolean indicating whether address is signer or not + */ + function isSigner(address signer) public view returns (bool) { + // Iterate through all signers on the wallet and + for (uint i = 0; i < signers.length; i++) { + if (signers[i] == signer) { + return true; + } + } + return false; + } + + /** + * Modifier that will execute internal code block only if the sender is an authorized signer on this wallet + */ + modifier onlySigner { + if (!isSigner(msg.sender)) { + revert(); + } + _; + } + + /** + * Gets called when a transaction is received without calling a method + */ + function() public payable { + if (msg.value > 0) { + // Fire deposited event if we are receiving funds + Deposited(msg.sender, msg.value, msg.data); + } + } + + /** + * Create a new contract (and also address) that forwards funds to this contract + * returns address of newly created forwarder address + */ + function createForwarder() public returns (address) { + return new Forwarder(); + } + + /** + * Execute a multi-signature transaction from this wallet using 2 signers: one from msg.sender and the other from ecrecover. + * Sequence IDs are numbers starting from 1. They are used to prevent replay attacks and may not be repeated. + * + * @param toAddress the destination address to send an outgoing transaction + * @param value the amount in Wei to be sent + * @param data the data to send to the toAddress when invoking the transaction + * @param expireTime the number of seconds since 1970 for which this transaction is valid + * @param sequenceId the unique sequence id obtainable from getNextSequenceId + * @param signature see Data Formats + */ + function sendMultiSig( + address toAddress, + uint value, + bytes data, + uint expireTime, + uint sequenceId, + bytes signature + ) public onlySigner { + // Verify the other signer + var operationHash = keccak256("ETC", toAddress, value, data, expireTime, sequenceId); + + var otherSigner = verifyMultiSig(toAddress, operationHash, signature, expireTime, sequenceId); + + // Success, send the transaction + if (!(toAddress.call.value(value)(data))) { + // Failed executing transaction + revert(); + } + Transacted(msg.sender, otherSigner, operationHash, toAddress, value, data); + } + + /** + * Execute a multi-signature token transfer from this wallet using 2 signers: one from msg.sender and the other from ecrecover. + * Sequence IDs are numbers starting from 1. They are used to prevent replay attacks and may not be repeated. + * + * @param toAddress the destination address to send an outgoing transaction + * @param value the amount in tokens to be sent + * @param tokenContractAddress the address of the erc20 token contract + * @param expireTime the number of seconds since 1970 for which this transaction is valid + * @param sequenceId the unique sequence id obtainable from getNextSequenceId + * @param signature see Data Formats + */ + function sendMultiSigToken( + address toAddress, + uint value, + address tokenContractAddress, + uint expireTime, + uint sequenceId, + bytes signature + ) public onlySigner { + // Verify the other signer + var operationHash = keccak256("ETC-ERC20", toAddress, value, tokenContractAddress, expireTime, sequenceId); + + verifyMultiSig(toAddress, operationHash, signature, expireTime, sequenceId); + + ERC20Interface instance = ERC20Interface(tokenContractAddress); + if (!instance.transfer(toAddress, value)) { + revert(); + } + } + + /** + * Execute a token flush from one of the forwarder addresses. This transfer needs only a single signature and can be done by any signer + * + * @param forwarderAddress the address of the forwarder address to flush the tokens from + * @param tokenContractAddress the address of the erc20 token contract + */ + function flushForwarderTokens( + address forwarderAddress, + address tokenContractAddress + ) public onlySigner { + Forwarder forwarder = Forwarder(forwarderAddress); + forwarder.flushTokens(tokenContractAddress); + } + + /** + * Do common multisig verification for both eth sends and erc20token transfers + * + * @param toAddress the destination address to send an outgoing transaction + * @param operationHash see Data Formats + * @param signature see Data Formats + * @param expireTime the number of seconds since 1970 for which this transaction is valid + * @param sequenceId the unique sequence id obtainable from getNextSequenceId + * returns address that has created the signature + */ + function verifyMultiSig( + address toAddress, + bytes32 operationHash, + bytes signature, + uint expireTime, + uint sequenceId + ) private returns (address) { + + var otherSigner = recoverAddressFromSignature(operationHash, signature); + + // Verify if we are in safe mode. In safe mode, the wallet can only send to signers + if (safeMode && !isSigner(toAddress)) { + // We are in safe mode and the toAddress is not a signer. Disallow! + revert(); + } + // Verify that the transaction has not expired + if (expireTime < block.timestamp) { + // Transaction expired + revert(); + } + + // Try to insert the sequence ID. Will revert if the sequence id was invalid + tryInsertSequenceId(sequenceId); + + if (!isSigner(otherSigner)) { + // Other signer not on this wallet or operation does not match arguments + revert(); + } + if (otherSigner == msg.sender) { + // Cannot approve own transaction + revert(); + } + + return otherSigner; + } + + /** + * Irrevocably puts contract into safe mode. When in this mode, transactions may only be sent to signing addresses. + */ + function activateSafeMode() public onlySigner { + safeMode = true; + SafeModeActivated(msg.sender); + } + + /** + * Gets signer's address using ecrecover + * @param operationHash see Data Formats + * @param signature see Data Formats + * returns address recovered from the signature + */ + function recoverAddressFromSignature( + bytes32 operationHash, + bytes signature + ) private pure returns (address) { + if (signature.length != 65) { + revert(); + } + // We need to unpack the signature, which is given as an array of 65 bytes (like eth.sign) + bytes32 r; + bytes32 s; + uint8 v; + assembly { + r := mload(add(signature, 32)) + s := mload(add(signature, 64)) + v := and(mload(add(signature, 65)), 255) + } + if (v < 27) { + v += 27; // Ethereum versions are 27 or 28 as opposed to 0 or 1 which is submitted by some signing libs + } + return ecrecover(operationHash, v, r, s); + } + + /** + * Verify that the sequence id has not been used before and inserts it. Throws if the sequence ID was not accepted. + * We collect a window of up to 10 recent sequence ids, and allow any sequence id that is not in the window and + * greater than the minimum element in the window. + * @param sequenceId to insert into array of stored ids + */ + function tryInsertSequenceId(uint sequenceId) private onlySigner { + // Keep a pointer to the lowest value element in the window + uint lowestValueIndex = 0; + for (uint i = 0; i < SEQUENCE_ID_WINDOW_SIZE; i++) { + if (recentSequenceIds[i] == sequenceId) { + // This sequence ID has been used before. Disallow! + revert(); + } + if (recentSequenceIds[i] < recentSequenceIds[lowestValueIndex]) { + lowestValueIndex = i; + } + } + if (sequenceId < recentSequenceIds[lowestValueIndex]) { + // The sequence ID being used is lower than the lowest value in the window + // so we cannot accept it as it may have been used before + revert(); + } + if (sequenceId > (recentSequenceIds[lowestValueIndex] + 10000)) { + // Block sequence IDs which are much higher than the lowest value + // This prevents people blocking the contract by using very large sequence IDs quickly + revert(); + } + recentSequenceIds[lowestValueIndex] = sequenceId; + } + + /** + * Gets the next available sequence ID for signing when using executeAndConfirm + * returns the sequenceId one higher than the highest currently stored + */ + function getNextSequenceId() public view returns (uint) { + uint highestSequenceId = 0; + for (uint i = 0; i < SEQUENCE_ID_WINDOW_SIZE; i++) { + if (recentSequenceIds[i] > highestSequenceId) { + highestSequenceId = recentSequenceIds[i]; + } + } + return highestSequenceId + 1; + } +} diff --git a/contracts/coins/RskWalletSimple.sol b/contracts/coins/RskWalletSimple.sol new file mode 100644 index 0000000..538fdad --- /dev/null +++ b/contracts/coins/RskWalletSimple.sol @@ -0,0 +1,312 @@ +pragma solidity ^0.4.18; +import "../Forwarder.sol"; +import "../ERC20Interface.sol"; +/** + * + * WalletSimple + * ============ + * + * Basic multi-signer wallet designed for use in a co-signing environment where 2 signatures are required to move funds. + * Typically used in a 2-of-3 signing configuration. Uses ecrecover to allow for 2 signatures in a single transaction. + * + * The first signature is created on the operation hash (see Data Formats) and passed to sendMultiSig/sendMultiSigToken + * The signer is determined by verifyMultiSig(). + * + * The second signature is created by the submitter of the transaction and determined by msg.signer. + * + * Data Formats + * ============ + * + * The signature is created with ethereumjs-util.ecsign(operationHash). + * Like the eth_sign RPC call, it packs the values as a 65-byte array of [r, s, v]. + * Unlike eth_sign, the message is not prefixed. + * + * The operationHash the result of keccak256(prefix, toAddress, value, data, expireTime). + * For ether transactions, `prefix` is "ETHER". + * For token transaction, `prefix` is "ERC20" and `data` is the tokenContractAddress. + * + * + */ +contract RskWalletSimple { + // Events + event Deposited(address from, uint value, bytes data); + event SafeModeActivated(address msgSender); + event Transacted( + address msgSender, // Address of the sender of the message initiating the transaction + address otherSigner, // Address of the signer (second signature) used to initiate the transaction + bytes32 operation, // Operation hash (see Data Formats) + address toAddress, // The address the transaction was sent to + uint value, // Amount of Wei sent to the address + bytes data // Data sent when invoking the transaction + ); + + // Public fields + address[] public signers; // The addresses that can co-sign transactions on the wallet + bool public safeMode = false; // When active, wallet may only send to signer addresses + + // Internal fields + uint constant SEQUENCE_ID_WINDOW_SIZE = 10; + uint[10] recentSequenceIds; + + /** + * Set up a simple multi-sig wallet by specifying the signers allowed to be used on this wallet. + * 2 signers will be required to send a transaction from this wallet. + * Note: The sender is NOT automatically added to the list of signers. + * Signers CANNOT be changed once they are set + * + * @param allowedSigners An array of signers on the wallet + */ + function RskWalletSimple(address[] allowedSigners) public { + if (allowedSigners.length != 3) { + // Invalid number of signers + revert(); + } + signers = allowedSigners; + } + + /** + * Determine if an address is a signer on this wallet + * @param signer address to check + * returns boolean indicating whether address is signer or not + */ + function isSigner(address signer) public view returns (bool) { + // Iterate through all signers on the wallet and + for (uint i = 0; i < signers.length; i++) { + if (signers[i] == signer) { + return true; + } + } + return false; + } + + /** + * Modifier that will execute internal code block only if the sender is an authorized signer on this wallet + */ + modifier onlySigner { + if (!isSigner(msg.sender)) { + revert(); + } + _; + } + + /** + * Gets called when a transaction is received without calling a method + */ + function() public payable { + if (msg.value > 0) { + // Fire deposited event if we are receiving funds + Deposited(msg.sender, msg.value, msg.data); + } + } + + /** + * Create a new contract (and also address) that forwards funds to this contract + * returns address of newly created forwarder address + */ + function createForwarder() public returns (address) { + return new Forwarder(); + } + + /** + * Execute a multi-signature transaction from this wallet using 2 signers: one from msg.sender and the other from ecrecover. + * Sequence IDs are numbers starting from 1. They are used to prevent replay attacks and may not be repeated. + * + * @param toAddress the destination address to send an outgoing transaction + * @param value the amount in Wei to be sent + * @param data the data to send to the toAddress when invoking the transaction + * @param expireTime the number of seconds since 1970 for which this transaction is valid + * @param sequenceId the unique sequence id obtainable from getNextSequenceId + * @param signature see Data Formats + */ + function sendMultiSig( + address toAddress, + uint value, + bytes data, + uint expireTime, + uint sequenceId, + bytes signature + ) public onlySigner { + // Verify the other signer + var operationHash = keccak256("RSK", toAddress, value, data, expireTime, sequenceId); + + var otherSigner = verifyMultiSig(toAddress, operationHash, signature, expireTime, sequenceId); + + // Success, send the transaction + if (!(toAddress.call.value(value)(data))) { + // Failed executing transaction + revert(); + } + Transacted(msg.sender, otherSigner, operationHash, toAddress, value, data); + } + + /** + * Execute a multi-signature token transfer from this wallet using 2 signers: one from msg.sender and the other from ecrecover. + * Sequence IDs are numbers starting from 1. They are used to prevent replay attacks and may not be repeated. + * + * @param toAddress the destination address to send an outgoing transaction + * @param value the amount in tokens to be sent + * @param tokenContractAddress the address of the erc20 token contract + * @param expireTime the number of seconds since 1970 for which this transaction is valid + * @param sequenceId the unique sequence id obtainable from getNextSequenceId + * @param signature see Data Formats + */ + function sendMultiSigToken( + address toAddress, + uint value, + address tokenContractAddress, + uint expireTime, + uint sequenceId, + bytes signature + ) public onlySigner { + // Verify the other signer + var operationHash = keccak256("RSK-ERC20", toAddress, value, tokenContractAddress, expireTime, sequenceId); + + verifyMultiSig(toAddress, operationHash, signature, expireTime, sequenceId); + + ERC20Interface instance = ERC20Interface(tokenContractAddress); + if (!instance.transfer(toAddress, value)) { + revert(); + } + } + + /** + * Execute a token flush from one of the forwarder addresses. This transfer needs only a single signature and can be done by any signer + * + * @param forwarderAddress the address of the forwarder address to flush the tokens from + * @param tokenContractAddress the address of the erc20 token contract + */ + function flushForwarderTokens( + address forwarderAddress, + address tokenContractAddress + ) public onlySigner { + Forwarder forwarder = Forwarder(forwarderAddress); + forwarder.flushTokens(tokenContractAddress); + } + + /** + * Do common multisig verification for both eth sends and erc20token transfers + * + * @param toAddress the destination address to send an outgoing transaction + * @param operationHash see Data Formats + * @param signature see Data Formats + * @param expireTime the number of seconds since 1970 for which this transaction is valid + * @param sequenceId the unique sequence id obtainable from getNextSequenceId + * returns address that has created the signature + */ + function verifyMultiSig( + address toAddress, + bytes32 operationHash, + bytes signature, + uint expireTime, + uint sequenceId + ) private returns (address) { + + var otherSigner = recoverAddressFromSignature(operationHash, signature); + + // Verify if we are in safe mode. In safe mode, the wallet can only send to signers + if (safeMode && !isSigner(toAddress)) { + // We are in safe mode and the toAddress is not a signer. Disallow! + revert(); + } + // Verify that the transaction has not expired + if (expireTime < block.timestamp) { + // Transaction expired + revert(); + } + + // Try to insert the sequence ID. Will revert if the sequence id was invalid + tryInsertSequenceId(sequenceId); + + if (!isSigner(otherSigner)) { + // Other signer not on this wallet or operation does not match arguments + revert(); + } + if (otherSigner == msg.sender) { + // Cannot approve own transaction + revert(); + } + + return otherSigner; + } + + /** + * Irrevocably puts contract into safe mode. When in this mode, transactions may only be sent to signing addresses. + */ + function activateSafeMode() public onlySigner { + safeMode = true; + SafeModeActivated(msg.sender); + } + + /** + * Gets signer's address using ecrecover + * @param operationHash see Data Formats + * @param signature see Data Formats + * returns address recovered from the signature + */ + function recoverAddressFromSignature( + bytes32 operationHash, + bytes signature + ) private pure returns (address) { + if (signature.length != 65) { + revert(); + } + // We need to unpack the signature, which is given as an array of 65 bytes (like eth.sign) + bytes32 r; + bytes32 s; + uint8 v; + assembly { + r := mload(add(signature, 32)) + s := mload(add(signature, 64)) + v := and(mload(add(signature, 65)), 255) + } + if (v < 27) { + v += 27; // Ethereum versions are 27 or 28 as opposed to 0 or 1 which is submitted by some signing libs + } + return ecrecover(operationHash, v, r, s); + } + + /** + * Verify that the sequence id has not been used before and inserts it. Throws if the sequence ID was not accepted. + * We collect a window of up to 10 recent sequence ids, and allow any sequence id that is not in the window and + * greater than the minimum element in the window. + * @param sequenceId to insert into array of stored ids + */ + function tryInsertSequenceId(uint sequenceId) private onlySigner { + // Keep a pointer to the lowest value element in the window + uint lowestValueIndex = 0; + for (uint i = 0; i < SEQUENCE_ID_WINDOW_SIZE; i++) { + if (recentSequenceIds[i] == sequenceId) { + // This sequence ID has been used before. Disallow! + revert(); + } + if (recentSequenceIds[i] < recentSequenceIds[lowestValueIndex]) { + lowestValueIndex = i; + } + } + if (sequenceId < recentSequenceIds[lowestValueIndex]) { + // The sequence ID being used is lower than the lowest value in the window + // so we cannot accept it as it may have been used before + revert(); + } + if (sequenceId > (recentSequenceIds[lowestValueIndex] + 10000)) { + // Block sequence IDs which are much higher than the lowest value + // This prevents people blocking the contract by using very large sequence IDs quickly + revert(); + } + recentSequenceIds[lowestValueIndex] = sequenceId; + } + + /** + * Gets the next available sequence ID for signing when using executeAndConfirm + * returns the sequenceId one higher than the highest currently stored + */ + function getNextSequenceId() public view returns (uint) { + uint highestSequenceId = 0; + for (uint i = 0; i < SEQUENCE_ID_WINDOW_SIZE; i++) { + if (recentSequenceIds[i] > highestSequenceId) { + highestSequenceId = recentSequenceIds[i]; + } + } + return highestSequenceId + 1; + } +} diff --git a/mocha-test/compile.js b/mocha-test/compile.js index 2d5685c..1853905 100644 --- a/mocha-test/compile.js +++ b/mocha-test/compile.js @@ -17,7 +17,11 @@ describe('Contracts', async () => { 'ERC20Interface.sol', 'FixedSupplyToken.sol', 'Forwarder.sol', - 'WalletSimple.sol' + 'WalletSimple.sol', + 'WalletSimple.sol', + 'coins/EtcWalletSimple.sol', + 'coins/RskWalletSimple.sol', + 'coins/CeloWalletSimple.sol', ]; let result; @@ -38,4 +42,4 @@ describe('Contracts', async () => { (result.errors || []).join('\n') ); }); -}); \ No newline at end of file +}); diff --git a/test/helpers.js b/test/helpers.js index 9f85146..e0ee9cd 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -36,18 +36,18 @@ exports.waitForEvents = function(eventsArray, numEvents) { }; // Helper to get sha3 for solidity tightly-packed arguments -exports.getSha3ForConfirmationTx = function(toAddress, amount, data, expireTime, sequenceId) { +exports.getSha3ForConfirmationTx = function(prefix, toAddress, amount, data, expireTime, sequenceId) { return abi.soliditySHA3( ['string', 'address', 'uint', 'string', 'uint', 'uint'], - ['ETHER', new BN(toAddress.replace('0x', ''), 16), web3.toWei(amount, 'ether'), data, expireTime, sequenceId] + [prefix, new BN(toAddress.replace('0x', ''), 16), web3.toWei(amount, 'ether'), data, expireTime, sequenceId] ); }; // Helper to get token transactions sha3 for solidity tightly-packed arguments -exports.getSha3ForConfirmationTokenTx = function(toAddress, value, tokenContractAddress, expireTime, sequenceId) { +exports.getSha3ForConfirmationTokenTx = function(prefix, toAddress, value, tokenContractAddress, expireTime, sequenceId) { return abi.soliditySHA3( ['string', 'address', 'uint', 'address', 'uint', 'uint'], - ['ERC20', new BN(toAddress.replace('0x', ''), 16), value, new BN(tokenContractAddress.replace('0x', ''), 16), expireTime, sequenceId] + [prefix, new BN(toAddress.replace('0x', ''), 16), value, new BN(tokenContractAddress.replace('0x', ''), 16), expireTime, sequenceId] ); }; diff --git a/test/walletsimple.js b/test/walletsimple.js index f90a1f0..c0058a3 100644 --- a/test/walletsimple.js +++ b/test/walletsimple.js @@ -10,7 +10,10 @@ const { privateKeyForAccount } = require('../testrpc/accounts'); const util = require('ethereumjs-util'); const crypto = require('crypto'); -const WalletSimple = artifacts.require('./WalletSimple.sol'); +const EthWalletSimple = artifacts.require('./WalletSimple.sol'); +const RskWalletSimple = artifacts.require('./RskWalletSimple.sol'); +const EtcWalletSimple = artifacts.require('./EtcWalletSimple.sol'); +const CeloWalletSimple = artifacts.require('./CeloWalletSimple.sol'); const Forwarder = artifacts.require('./Forwarder.sol'); const FixedSupplyToken = artifacts.require('./FixedSupplyToken.sol'); @@ -24,106 +27,134 @@ const createForwarderFromWallet = async (wallet) => { return Forwarder.at(forwarderAddress); }; -contract('WalletSimple', function(accounts) { - let wallet; - let walletEvents; - let watcher; - - // Set up and tear down events logging on all tests. the wallet will be set up in the before() of each test block. - beforeEach(function() { - if (wallet) { - walletEvents = []; - // Set up event watcher - watcher = wallet.allEvents({}, function (error, event) { - walletEvents.push(event); - }); - } - }); - afterEach(function() { - if (watcher) { - watcher.stopWatching(); - } - }); - - // Taken from http://solidity.readthedocs.io/en/latest/frequently-asked-questions.html - - // The automatic accessor function for a public state variable of array type only returns individual elements. - // If you want to return the complete array, you have to manually write a function to do that. - const getSigners = async function getSigners(wallet) { - const signers = []; - let i = 0; - while (true) { - try { - const signer = await wallet.signers.call(i++); - signers.push(signer); - } catch (e) { - break; +const coins = [ + { + name: 'Eth', + nativePrefix: 'ETHER', + tokenPrefix: 'ERC20', + WalletSimple: EthWalletSimple, + }, + { + name: 'Rsk', + nativePrefix: 'RSK', + tokenPrefix: 'RSK-ERC20', + WalletSimple: RskWalletSimple, + }, + { + name: 'Etc', + nativePrefix: 'ETC', + tokenPrefix: 'ETC-ERC20', + WalletSimple: EtcWalletSimple, + }, + { + name: 'Celo', + nativePrefix: 'CELO', + tokenPrefix: 'CELO-ERC20', + WalletSimple: CeloWalletSimple, + }, +]; + +coins.forEach(({ name: coinName, nativePrefix, tokenPrefix, WalletSimple }) => { + contract(`${coinName}WalletSimple`, function(accounts) { + let wallet; + let walletEvents; + let watcher; + + // Set up and tear down events logging on all tests. the wallet will be set up in the before() of each test block. + beforeEach(function() { + if (wallet) { + walletEvents = []; + // Set up event watcher + watcher = wallet.allEvents({}, function (error, event) { + walletEvents.push(event); + }); } - } - return signers; - }; - - describe('Wallet creation', function() { - it('2 of 3 multisig wallet', async function() { - const wallet = await WalletSimple.new([accounts[0], accounts[1], accounts[2]]); - - const signers = await getSigners(wallet); - signers.should.eql([accounts[0], accounts[1], accounts[2]]); - - const isSafeMode = await wallet.safeMode.call(); - isSafeMode.should.eql(false); - - const isSignerArray = await Promise.all([ - wallet.isSigner.call(accounts[0]), - wallet.isSigner.call(accounts[1]), - wallet.isSigner.call(accounts[2]), - wallet.isSigner.call(accounts[3]) - ]); - - isSignerArray.length.should.eql(4); - isSignerArray[0].should.eql(true); - isSignerArray[1].should.eql(true); - isSignerArray[2].should.eql(true); - isSignerArray[3].should.eql(false); }); - - it('Not enough signer addresses', async function() { - try { - await WalletSimple.new([accounts[0]]); - throw new Error('should not be here'); - } catch(e) { - e.message.should.not.containEql('should not be here'); + afterEach(function() { + if (watcher) { + watcher.stopWatching(); } }); - }); - describe('Deposits', function() { - before(async function() { - wallet = await WalletSimple.new([accounts[0], accounts[1], accounts[2]]); - }); + // Taken from http://solidity.readthedocs.io/en/latest/frequently-asked-questions.html - + // The automatic accessor function for a public state variable of array type only returns individual elements. + // If you want to return the complete array, you have to manually write a function to do that. + const getSigners = async function getSigners(wallet) { + const signers = []; + let i = 0; + while (true) { + try { + const signer = await wallet.signers.call(i++); + signers.push(signer); + } catch (e) { + break; + } + } + return signers; + }; + + describe('Wallet creation', function() { + it('2 of 3 multisig wallet', async function() { + const wallet = await WalletSimple.new([accounts[0], accounts[1], accounts[2]]); - it('Should emit event on deposit', async function() { - web3.eth.sendTransaction({ from: accounts[0], to: wallet.address, value: web3.toWei(20, 'ether') }); - await helpers.waitForEvents(walletEvents, 1); // wait for events to come in - const depositEvent = _.find(walletEvents, function(event) { - return event.event === 'Deposited'; + const signers = await getSigners(wallet); + signers.should.eql([accounts[0], accounts[1], accounts[2]]); + + const isSafeMode = await wallet.safeMode.call(); + isSafeMode.should.eql(false); + + const isSignerArray = await Promise.all([ + wallet.isSigner.call(accounts[0]), + wallet.isSigner.call(accounts[1]), + wallet.isSigner.call(accounts[2]), + wallet.isSigner.call(accounts[3]) + ]); + + isSignerArray.length.should.eql(4); + isSignerArray[0].should.eql(true); + isSignerArray[1].should.eql(true); + isSignerArray[2].should.eql(true); + isSignerArray[3].should.eql(false); + }); + + it('Not enough signer addresses', async function() { + try { + await WalletSimple.new([accounts[0]]); + throw new Error('should not be here'); + } catch(e) { + e.message.should.not.containEql('should not be here'); + } }); - depositEvent.args.from.should.eql(accounts[0]); - depositEvent.args.value.should.eql(web3.toBigNumber(web3.toWei(20, 'ether'))); }); - it('Should emit event with data on deposit', async function() { - web3.eth.sendTransaction({ from: accounts[0], to: wallet.address, value: web3.toWei(30, 'ether'), data: '0xabcd' }); - await helpers.waitForEvents(walletEvents, 1); // wait for events to come in - const depositEvent = _.find(walletEvents, function(event) { - return event.event === 'Deposited'; + describe('Deposits', function() { + before(async function() { + wallet = await WalletSimple.new([accounts[0], accounts[1], accounts[2]]); + }); + + it('Should emit event on deposit', async function() { + web3.eth.sendTransaction({ from: accounts[0], to: wallet.address, value: web3.toWei(20, 'ether') }); + await helpers.waitForEvents(walletEvents, 1); // wait for events to come in + const depositEvent = _.find(walletEvents, function(event) { + return event.event === 'Deposited'; + }); + depositEvent.args.from.should.eql(accounts[0]); + depositEvent.args.value.should.eql(web3.toBigNumber(web3.toWei(20, 'ether'))); + }); + + it('Should emit event with data on deposit', async function() { + web3.eth.sendTransaction({ from: accounts[0], to: wallet.address, value: web3.toWei(30, 'ether'), data: '0xabcd' }); + await helpers.waitForEvents(walletEvents, 1); // wait for events to come in + const depositEvent = _.find(walletEvents, function(event) { + return event.event === 'Deposited'; + }); + depositEvent.args.from.should.eql(accounts[0]); + depositEvent.args.value.should.eql(web3.toBigNumber(web3.toWei(30, 'ether'))); + depositEvent.args.data.should.eql('0xabcd'); }); - depositEvent.args.from.should.eql(accounts[0]); - depositEvent.args.value.should.eql(web3.toBigNumber(web3.toWei(30, 'ether'))); - depositEvent.args.data.should.eql('0xabcd'); }); - }); - /* + /* Commented out because tryInsertSequenceId and recoverAddressFromSignature is private. Uncomment the private and tests to test this. Functionality is also tested in the sendMultiSig tests. @@ -181,7 +212,7 @@ contract('WalletSimple', function(accounts) { assertVMException(err); } - // should be unchanged + // should be unchanged const newSequenceId = await getSequenceId(); sequenceId.should.eql(newSequenceId); }); @@ -201,13 +232,13 @@ contract('WalletSimple', function(accounts) { const windowSize = 10; let sequenceId = await getSequenceId(); const originalNextSequenceId = sequenceId; - // Try for 9 times (windowsize - 1) because the last window was used already + // Try for 9 times (windowsize - 1) because the last window was used already for (let i=0; i < (windowSize - 1); i++) { sequenceId -= 5; // since we were incrementing 100 per time, this should be unused await wallet.tryInsertSequenceId(sequenceId, { from: accounts[0] }); } const newSequenceId = await getSequenceId(); - // we should still get the same next sequence id since we were using old ids + // we should still get the same next sequence id since we were using old ids newSequenceId.should.eql(originalNextSequenceId); }); @@ -233,154 +264,111 @@ contract('WalletSimple', function(accounts) { }); */ - // Helper to get the operation hash, sign it, and then send it using sendMultiSig - const sendMultiSigTestHelper = async function(params) { - assert(params.msgSenderAddress); - assert(params.otherSignerAddress); - assert(params.wallet); - - assert(params.toAddress); - assert(params.amount); - assert(params.data === '' || params.data); - assert(params.expireTime); - assert(params.sequenceId); - - // For testing, allow arguments to override the parameters above, - // as if the other signer or message sender were changing them - const otherSignerArgs = _.extend({}, params, params.otherSignerArgs); - const msgSenderArgs = _.extend({}, params, params.msgSenderArgs); - - // Get the operation hash to be signed - const operationHash = helpers.getSha3ForConfirmationTx( - otherSignerArgs.toAddress, - otherSignerArgs.amount, - otherSignerArgs.data, - otherSignerArgs.expireTime, - otherSignerArgs.sequenceId - ); - const sig = util.ecsign(operationHash, privateKeyForAccount(params.otherSignerAddress)); - - await params.wallet.sendMultiSig( - msgSenderArgs.toAddress, - web3.toWei(msgSenderArgs.amount, 'ether'), - msgSenderArgs.data, - msgSenderArgs.expireTime, - msgSenderArgs.sequenceId, - helpers.serializeSignature(sig), - { from: params.msgSenderAddress } - ); - }; - - // Helper to expect successful execute and confirm - const expectSuccessfulSendMultiSig = async function(params) { - const destinationAccountStartEther = web3.fromWei(web3.eth.getBalance(params.toAddress), 'ether'); - const msigWalletStartEther = web3.fromWei(web3.eth.getBalance(params.wallet.address), 'ether'); - - const result = await sendMultiSigTestHelper(params); - - // Check the post-transaction balances - const destinationAccountEndEther = web3.fromWei(web3.eth.getBalance(params.toAddress), 'ether'); - destinationAccountStartEther.plus(params.amount).should.eql(destinationAccountEndEther); - const msigWalletEndEther = web3.fromWei(web3.eth.getBalance(params.wallet.address), 'ether'); - msigWalletStartEther.minus(params.amount).should.eql(msigWalletEndEther); - - return result; - }; - - // Helper to expect failed execute and confirm - const expectFailSendMultiSig = async function(params) { - const destinationAccountStartEther = web3.fromWei(web3.eth.getBalance(params.toAddress), 'ether'); - const msigWalletStartEther = web3.fromWei(web3.eth.getBalance(params.wallet.address), 'ether'); - - try { - await sendMultiSigTestHelper(params); - throw new Error('should not have sent successfully'); - } catch(err) { - assertVMException(err); - } - - // Check the balances after the transaction - const destinationAccountEndEther = web3.fromWei(web3.eth.getBalance(params.toAddress), 'ether'); - destinationAccountStartEther.plus(0).should.eql(destinationAccountEndEther); - const msigWalletEndEther = web3.fromWei(web3.eth.getBalance(params.wallet.address), 'ether'); - msigWalletStartEther.minus(0).should.eql(msigWalletEndEther); - }; - - describe('Transaction sending using sendMultiSig', function() { - before(async function() { - // Create and fund the wallet - wallet = await WalletSimple.new([accounts[0], accounts[1], accounts[2]]); - web3.eth.sendTransaction({ from: accounts[0], to: wallet.address, value: web3.toWei(200000, 'ether') }); - web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(200000)); - }); - let sequenceId; - beforeEach(async function() { - // Run before each test. Sets the sequence ID up to be used in the tests - const sequenceIdString = await wallet.getNextSequenceId.call(); - sequenceId = parseInt(sequenceIdString); - }); + // Helper to get the operation hash, sign it, and then send it using sendMultiSig + const sendMultiSigTestHelper = async function(params) { + assert(params.msgSenderAddress); + assert(params.otherSignerAddress); + assert(params.wallet); + + assert(params.toAddress); + assert(params.amount); + assert(params.data === '' || params.data); + assert(params.expireTime); + assert(params.sequenceId); + + // For testing, allow arguments to override the parameters above, + // as if the other signer or message sender were changing them + const otherSignerArgs = _.extend({}, params, params.otherSignerArgs); + const msgSenderArgs = _.extend({}, params, params.msgSenderArgs); + + // Get the operation hash to be signed + const operationHash = helpers.getSha3ForConfirmationTx( + params.prefix || nativePrefix, + otherSignerArgs.toAddress, + otherSignerArgs.amount, + otherSignerArgs.data, + otherSignerArgs.expireTime, + otherSignerArgs.sequenceId + ); + const sig = util.ecsign(operationHash, privateKeyForAccount(params.otherSignerAddress)); + + await params.wallet.sendMultiSig( + msgSenderArgs.toAddress, + web3.toWei(msgSenderArgs.amount, 'ether'), + msgSenderArgs.data, + msgSenderArgs.expireTime, + msgSenderArgs.sequenceId, + helpers.serializeSignature(sig), + { from: params.msgSenderAddress } + ); + }; - it('Send out 50 ether with sendMultiSig', async function() { - // We are not using the helper here because we want to check the operation hash in events - const destinationAccount = accounts[5]; - const amount = 50; - const expireTime = Math.floor((new Date().getTime()) / 1000) + 60; // 60 seconds - const data = 'abcde35f123'; + // Helper to expect successful execute and confirm + const expectSuccessfulSendMultiSig = async function(params) { + const destinationAccountStartEther = web3.fromWei(web3.eth.getBalance(params.toAddress), 'ether'); + const msigWalletStartEther = web3.fromWei(web3.eth.getBalance(params.wallet.address), 'ether'); - const destinationAccountStartEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); - const msigWalletStartEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); + const result = await sendMultiSigTestHelper(params); - const operationHash = helpers.getSha3ForConfirmationTx(destinationAccount, amount, data, expireTime, sequenceId); - const sig = util.ecsign(operationHash, privateKeyForAccount(accounts[1])); + // Check the post-transaction balances + const destinationAccountEndEther = web3.fromWei(web3.eth.getBalance(params.toAddress), 'ether'); + destinationAccountStartEther.plus(params.amount).should.eql(destinationAccountEndEther); + const msigWalletEndEther = web3.fromWei(web3.eth.getBalance(params.wallet.address), 'ether'); + msigWalletStartEther.minus(params.amount).should.eql(msigWalletEndEther); - await wallet.sendMultiSig( - destinationAccount, web3.toWei(amount, 'ether'), data, expireTime, sequenceId, helpers.serializeSignature(sig), - { from: accounts[0] } - ); - const destinationAccountEndEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); - destinationAccountStartEther.plus(amount).should.eql(destinationAccountEndEther); + return result; + }; + + // Helper to expect failed execute and confirm + const expectFailSendMultiSig = async function(params) { + const destinationAccountStartEther = web3.fromWei(web3.eth.getBalance(params.toAddress), 'ether'); + const msigWalletStartEther = web3.fromWei(web3.eth.getBalance(params.wallet.address), 'ether'); - // Check wallet balance - const msigWalletEndEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); - msigWalletStartEther.minus(amount).should.eql(msigWalletEndEther); + try { + await sendMultiSigTestHelper(params); + throw new Error('should not have sent successfully'); + } catch(err) { + assertVMException(err); + } - await helpers.waitForEvents(walletEvents, 2); // wait for events to come in + // Check the balances after the transaction + const destinationAccountEndEther = web3.fromWei(web3.eth.getBalance(params.toAddress), 'ether'); + destinationAccountStartEther.plus(0).should.eql(destinationAccountEndEther); + const msigWalletEndEther = web3.fromWei(web3.eth.getBalance(params.wallet.address), 'ether'); + msigWalletStartEther.minus(0).should.eql(msigWalletEndEther); + }; - // Check wallet events for Transacted event - const transactedEvent = _.find(walletEvents, function(event) { - return event.event === 'Transacted'; + describe('Transaction sending using sendMultiSig', function() { + before(async function() { + // Create and fund the wallet + wallet = await WalletSimple.new([accounts[0], accounts[1], accounts[2]]); + web3.eth.sendTransaction({ from: accounts[0], to: wallet.address, value: web3.toWei(200000, 'ether') }); + web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(200000)); + }); + let sequenceId; + beforeEach(async function() { + // Run before each test. Sets the sequence ID up to be used in the tests + const sequenceIdString = await wallet.getNextSequenceId.call(); + sequenceId = parseInt(sequenceIdString); }); - transactedEvent.args.msgSender.should.eql(accounts[0]); - transactedEvent.args.otherSigner.should.eql(accounts[1]); - transactedEvent.args.operation.should.eql(util.addHexPrefix(operationHash.toString('hex'))); - transactedEvent.args.value.should.eql(web3.toBigNumber(web3.toWei(amount, 'ether'))); - transactedEvent.args.toAddress.should.eql(destinationAccount); - transactedEvent.args.data.should.eql(util.addHexPrefix(new Buffer(data).toString('hex'))); - }); - it('Stress test: 20 rounds of sendMultiSig', async function() { - for (let round=0; round < 20; round++) { - const destinationAccount = accounts[2]; - const amount = _.random(1,9); + it('Send out 50 ether with sendMultiSig', async function() { + // We are not using the helper here because we want to check the operation hash in events + const destinationAccount = accounts[5]; + const amount = 50; const expireTime = Math.floor((new Date().getTime()) / 1000) + 60; // 60 seconds - const data = crypto.randomBytes(20).toString('hex'); - - const operationHash = helpers.getSha3ForConfirmationTx(destinationAccount, amount, data, expireTime, sequenceId); - const sig = util.ecsign(operationHash, privateKeyForAccount(accounts[0])); - - console.log( - 'ExpectSuccess ' + round + ': ' + amount + 'ETH, seqId: ' + sequenceId + - ', operationHash: ' + operationHash.toString('hex') + ', sig: ' + helpers.serializeSignature(sig) - ); + const data = 'abcde35f123'; const destinationAccountStartEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); const msigWalletStartEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); + + const operationHash = helpers.getSha3ForConfirmationTx(nativePrefix, destinationAccount, amount, data, expireTime, sequenceId); + const sig = util.ecsign(operationHash, privateKeyForAccount(accounts[1])); + await wallet.sendMultiSig( destinationAccount, web3.toWei(amount, 'ether'), data, expireTime, sequenceId, helpers.serializeSignature(sig), - { from: accounts[1] } + { from: accounts[0] } ); - - // Check other account balance const destinationAccountEndEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); destinationAccountStartEther.plus(amount).should.eql(destinationAccountEndEther); @@ -388,569 +376,633 @@ contract('WalletSimple', function(accounts) { const msigWalletEndEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); msigWalletStartEther.minus(amount).should.eql(msigWalletEndEther); - // Increment sequence id - sequenceId++; - } - }); + await helpers.waitForEvents(walletEvents, 2); // wait for events to come in + + // Check wallet events for Transacted event + const transactedEvent = _.find(walletEvents, function(event) { + return event.event === 'Transacted'; + }); + transactedEvent.args.msgSender.should.eql(accounts[0]); + transactedEvent.args.otherSigner.should.eql(accounts[1]); + transactedEvent.args.operation.should.eql(util.addHexPrefix(operationHash.toString('hex'))); + transactedEvent.args.value.should.eql(web3.toBigNumber(web3.toWei(amount, 'ether'))); + transactedEvent.args.toAddress.should.eql(destinationAccount); + transactedEvent.args.data.should.eql(util.addHexPrefix(new Buffer(data).toString('hex'))); + }); - it('Stress test: 10 rounds of attempting to reuse sequence ids - should fail', async function() { - sequenceId -= 10; // these sequence ids already used - for (let round=0; round < 10; round++) { - const destinationAccount = accounts[2]; - const amount = _.random(1,9); - const expireTime = Math.floor((new Date().getTime()) / 1000) + 60; // 60 seconds - const data = crypto.randomBytes(20).toString('hex'); + it('Stress test: 20 rounds of sendMultiSig', async function() { + for (let round=0; round < 20; round++) { + const destinationAccount = accounts[2]; + const amount = _.random(1,9); + const expireTime = Math.floor((new Date().getTime()) / 1000) + 60; // 60 seconds + const data = crypto.randomBytes(20).toString('hex'); - const operationHash = helpers.getSha3ForConfirmationTx(destinationAccount, amount, data, expireTime, sequenceId); - const sig = util.ecsign(operationHash, privateKeyForAccount(accounts[0])); + const operationHash = helpers.getSha3ForConfirmationTx(nativePrefix, destinationAccount, amount, data, expireTime, sequenceId); + const sig = util.ecsign(operationHash, privateKeyForAccount(accounts[0])); - console.log( - 'ExpectFail ' + round + ': ' + amount + 'ETH, seqId: ' + sequenceId + - ', operationHash: ' + operationHash.toString('hex') + ', sig: ' + helpers.serializeSignature(sig) - ); - const destinationAccountStartEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); - const msigWalletStartEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); - try { + console.log( + 'ExpectSuccess ' + round + ': ' + amount + 'ETH, seqId: ' + sequenceId + + ', operationHash: ' + operationHash.toString('hex') + ', sig: ' + helpers.serializeSignature(sig) + ); + + const destinationAccountStartEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); + const msigWalletStartEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); await wallet.sendMultiSig( destinationAccount, web3.toWei(amount, 'ether'), data, expireTime, sequenceId, helpers.serializeSignature(sig), { from: accounts[1] } ); - throw new Error('should not be here'); - } catch(err) { - assertVMException(err); - } - - // Check other account balance - const destinationAccountEndEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); - destinationAccountStartEther.plus(0).should.eql(destinationAccountEndEther); - // Check wallet balance - const msigWalletEndEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); - msigWalletStartEther.minus(0).should.eql(msigWalletEndEther); + // Check other account balance + const destinationAccountEndEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); + destinationAccountStartEther.plus(amount).should.eql(destinationAccountEndEther); - // Increment sequence id - sequenceId++; - } - }); + // Check wallet balance + const msigWalletEndEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); + msigWalletStartEther.minus(amount).should.eql(msigWalletEndEther); - it('Stress test: 20 rounds of confirming in a single tx from an incorrect sender - should fail', async function() { - const sequenceIdString = await wallet.getNextSequenceId.call(); - sequenceId = parseInt(sequenceIdString); + // Increment sequence id + sequenceId++; + } + }); - for (let round=0; round < 20; round++) { - const destinationAccount = accounts[2]; - const amount = _.random(1,9); - const expireTime = Math.floor((new Date().getTime()) / 1000) + 60; // 60 seconds - const data = crypto.randomBytes(20).toString('hex'); + it('Stress test: 10 rounds of attempting to reuse sequence ids - should fail', async function() { + sequenceId -= 10; // these sequence ids already used + for (let round=0; round < 10; round++) { + const destinationAccount = accounts[2]; + const amount = _.random(1,9); + const expireTime = Math.floor((new Date().getTime()) / 1000) + 60; // 60 seconds + const data = crypto.randomBytes(20).toString('hex'); - const operationHash = helpers.getSha3ForConfirmationTx(destinationAccount, amount, data, expireTime, sequenceId); - const sig = util.ecsign(operationHash, privateKeyForAccount(accounts[5+round%5])); + const operationHash = helpers.getSha3ForConfirmationTx(nativePrefix, destinationAccount, amount, data, expireTime, sequenceId); + const sig = util.ecsign(operationHash, privateKeyForAccount(accounts[0])); - console.log( - 'ExpectFail ' + round + ': ' + amount + 'ETH, seqId: ' + sequenceId + - ', operationHash: ' + operationHash.toString('hex') + ', sig: ' + helpers.serializeSignature(sig)); - const destinationAccountStartEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); - const msigWalletStartEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); - try { - await wallet.sendMultiSig( - destinationAccount, web3.toWei(amount, 'ether'), data, expireTime, sequenceId, helpers.serializeSignature(sig), - { from: accounts[1] } + console.log( + 'ExpectFail ' + round + ': ' + amount + 'ETH, seqId: ' + sequenceId + + ', operationHash: ' + operationHash.toString('hex') + ', sig: ' + helpers.serializeSignature(sig) ); - throw new Error('should not be here'); - } catch(err) { - assertVMException(err); + const destinationAccountStartEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); + const msigWalletStartEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); + try { + await wallet.sendMultiSig( + destinationAccount, web3.toWei(amount, 'ether'), data, expireTime, sequenceId, helpers.serializeSignature(sig), + { from: accounts[1] } + ); + throw new Error('should not be here'); + } catch(err) { + assertVMException(err); + } + + // Check other account balance + const destinationAccountEndEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); + destinationAccountStartEther.plus(0).should.eql(destinationAccountEndEther); + + // Check wallet balance + const msigWalletEndEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); + msigWalletStartEther.minus(0).should.eql(msigWalletEndEther); + + // Increment sequence id + sequenceId++; } + }); - // Check other account balance - const destinationAccountEndEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); - destinationAccountStartEther.plus(0).should.eql(destinationAccountEndEther); + it('Stress test: 20 rounds of confirming in a single tx from an incorrect sender - should fail', async function() { + const sequenceIdString = await wallet.getNextSequenceId.call(); + sequenceId = parseInt(sequenceIdString); + + for (let round=0; round < 20; round++) { + const destinationAccount = accounts[2]; + const amount = _.random(1,9); + const expireTime = Math.floor((new Date().getTime()) / 1000) + 60; // 60 seconds + const data = crypto.randomBytes(20).toString('hex'); + + const operationHash = helpers.getSha3ForConfirmationTx(nativePrefix, destinationAccount, amount, data, expireTime, sequenceId); + const sig = util.ecsign(operationHash, privateKeyForAccount(accounts[5+round%5])); + + console.log( + 'ExpectFail ' + round + ': ' + amount + 'ETH, seqId: ' + sequenceId + + ', operationHash: ' + operationHash.toString('hex') + ', sig: ' + helpers.serializeSignature(sig)); + const destinationAccountStartEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); + const msigWalletStartEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); + try { + await wallet.sendMultiSig( + destinationAccount, web3.toWei(amount, 'ether'), data, expireTime, sequenceId, helpers.serializeSignature(sig), + { from: accounts[1] } + ); + throw new Error('should not be here'); + } catch(err) { + assertVMException(err); + } + + // Check other account balance + const destinationAccountEndEther = web3.fromWei(web3.eth.getBalance(destinationAccount), 'ether'); + destinationAccountStartEther.plus(0).should.eql(destinationAccountEndEther); + + // Check wallet balance + const msigWalletEndEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); + msigWalletStartEther.minus(0).should.eql(msigWalletEndEther); + + // Increment sequence id + sequenceId++; + } + }); - // Check wallet balance - const msigWalletEndEther = web3.fromWei(web3.eth.getBalance(wallet.address), 'ether'); - msigWalletStartEther.minus(0).should.eql(msigWalletEndEther); + it('Msg sender changing the amount should fail', async function() { + const params = { + msgSenderAddress: accounts[0], + otherSignerAddress: accounts[1], + wallet: wallet, + toAddress: accounts[8], + amount: 15, + data: '', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: sequenceId + }; + + // override with different amount + params.msgSenderArgs = { + amount: 20 + }; + + await expectFailSendMultiSig(params); + }); - // Increment sequence id - sequenceId++; - } - }); + it('Msg sender changing the destination account should fail', async function() { + const params = { + msgSenderAddress: accounts[1], + otherSignerAddress: accounts[0], + wallet: wallet, + toAddress: accounts[5], + amount: 25, + data: '001122ee', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: sequenceId + }; + + // override with different amount + params.msgSenderArgs = { + toAddress: accounts[6] + }; + + await expectFailSendMultiSig(params); + }); - it('Msg sender changing the amount should fail', async function() { - const params = { - msgSenderAddress: accounts[0], - otherSignerAddress: accounts[1], - wallet: wallet, - toAddress: accounts[8], - amount: 15, - data: '', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: sequenceId - }; - - // override with different amount - params.msgSenderArgs = { - amount: 20 - }; - - await expectFailSendMultiSig(params); - }); + it('Msg sender changing the data should fail', async function() { + const params = { + msgSenderAddress: accounts[1], + otherSignerAddress: accounts[2], + wallet: wallet, + toAddress: accounts[0], + amount: 30, + data: 'abcdef', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: sequenceId + }; + + // override with different amount + params.msgSenderArgs = { + data: '12bcde' + }; + + await expectFailSendMultiSig(params); + }); - it('Msg sender changing the destination account should fail', async function() { - const params = { - msgSenderAddress: accounts[1], - otherSignerAddress: accounts[0], - wallet: wallet, - toAddress: accounts[5], - amount: 25, - data: '001122ee', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: sequenceId - }; - - // override with different amount - params.msgSenderArgs = { - toAddress: accounts[6] - }; - - await expectFailSendMultiSig(params); - }); + it('Msg sender changing the expire time should fail', async function() { + const params = { + msgSenderAddress: accounts[0], + otherSignerAddress: accounts[1], + wallet: wallet, + toAddress: accounts[2], + amount: 50, + data: 'abcdef', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: sequenceId + }; + + // override with different amount + params.msgSenderArgs = { + expireTime: Math.floor((new Date().getTime()) / 1000) + 1000 + }; + + await expectFailSendMultiSig(params); + }); - it('Msg sender changing the data should fail', async function() { - const params = { - msgSenderAddress: accounts[1], - otherSignerAddress: accounts[2], - wallet: wallet, - toAddress: accounts[0], - amount: 30, - data: 'abcdef', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: sequenceId - }; - - // override with different amount - params.msgSenderArgs = { - data: '12bcde' - }; - - await expectFailSendMultiSig(params); - }); + it('Same owner signing twice should fail', async function() { + const params = { + msgSenderAddress: accounts[2], + otherSignerAddress: accounts[2], + wallet: wallet, + toAddress: accounts[9], + amount: 51, + data: 'abcdef', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: sequenceId + }; + + await expectFailSendMultiSig(params); + }); - it('Msg sender changing the expire time should fail', async function() { - const params = { - msgSenderAddress: accounts[0], - otherSignerAddress: accounts[1], - wallet: wallet, - toAddress: accounts[2], - amount: 50, - data: 'abcdef', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: sequenceId - }; - - // override with different amount - params.msgSenderArgs = { - expireTime: Math.floor((new Date().getTime()) / 1000) + 1000 - }; - - await expectFailSendMultiSig(params); - }); + it('Sending from an unauthorized signer (but valid other signature) should fail', async function() { + const params = { + msgSenderAddress: accounts[7], + otherSignerAddress: accounts[2], + wallet: wallet, + toAddress: accounts[1], + amount: 52, + data: '', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: sequenceId + }; + + await expectFailSendMultiSig(params); + }); - it('Same owner signing twice should fail', async function() { - const params = { - msgSenderAddress: accounts[2], - otherSignerAddress: accounts[2], - wallet: wallet, - toAddress: accounts[9], - amount: 51, - data: 'abcdef', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: sequenceId - }; - - await expectFailSendMultiSig(params); - }); + it('Sending from an authorized signer (but unauthorized other signer) should fail', async function() { + const params = { + msgSenderAddress: accounts[0], + otherSignerAddress: accounts[6], + wallet: wallet, + toAddress: accounts[6], + amount: 53, + data: 'ab1234', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: sequenceId + }; + + await expectFailSendMultiSig(params); + }); - it('Sending from an unauthorized signer (but valid other signature) should fail', async function() { - const params = { - msgSenderAddress: accounts[7], - otherSignerAddress: accounts[2], - wallet: wallet, - toAddress: accounts[1], - amount: 52, - data: '', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: sequenceId - }; - - await expectFailSendMultiSig(params); - }); + let usedSequenceId; + it('Sending with expireTime very far out should work', async function() { + const params = { + msgSenderAddress: accounts[0], + otherSignerAddress: accounts[1], + wallet: wallet, + toAddress: accounts[5], + amount: 60, + data: '', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: sequenceId + }; + + await expectSuccessfulSendMultiSig(params); + usedSequenceId = sequenceId; + }); - it('Sending from an authorized signer (but unauthorized other signer) should fail', async function() { - const params = { - msgSenderAddress: accounts[0], - otherSignerAddress: accounts[6], - wallet: wallet, - toAddress: accounts[6], - amount: 53, - data: 'ab1234', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: sequenceId - }; - - await expectFailSendMultiSig(params); - }); + it('Sending with expireTime in the past should fail', async function() { + const params = { + msgSenderAddress: accounts[0], + otherSignerAddress: accounts[2], + wallet: wallet, + toAddress: accounts[2], + amount: 55, + data: 'aa', + expireTime: Math.floor((new Date().getTime()) / 1000) - 100, + sequenceId: sequenceId + }; + + await expectFailSendMultiSig(params); + }); - let usedSequenceId; - it('Sending with expireTime very far out should work', async function() { - const params = { - msgSenderAddress: accounts[0], - otherSignerAddress: accounts[1], - wallet: wallet, - toAddress: accounts[5], - amount: 60, - data: '', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: sequenceId - }; - - await expectSuccessfulSendMultiSig(params); - usedSequenceId = sequenceId; - }); + it('Can send with a sequence ID that is not sequential but higher than previous', async function() { + sequenceId = 1000; + const params = { + msgSenderAddress: accounts[1], + otherSignerAddress: accounts[2], + wallet: wallet, + toAddress: accounts[5], + amount: 60, + data: 'abcde35f123', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: sequenceId + }; + + await expectSuccessfulSendMultiSig(params); + }); - it('Sending with expireTime in the past should fail', async function() { - const params = { - msgSenderAddress: accounts[0], - otherSignerAddress: accounts[2], - wallet: wallet, - toAddress: accounts[2], - amount: 55, - data: 'aa', - expireTime: Math.floor((new Date().getTime()) / 1000) - 100, - sequenceId: sequenceId - }; - - await expectFailSendMultiSig(params); - }); + it('Can send with a sequence ID that is unused but lower than the previous (not strictly monotonic increase)', async function() { + sequenceId = 200; + const params = { + msgSenderAddress: accounts[0], + otherSignerAddress: accounts[1], + wallet: wallet, + toAddress: accounts[5], + amount: 61, + data: '100135f123', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: sequenceId + }; + + await expectSuccessfulSendMultiSig(params); + }); - it('Can send with a sequence ID that is not sequential but higher than previous', async function() { - sequenceId = 1000; - const params = { - msgSenderAddress: accounts[1], - otherSignerAddress: accounts[2], - wallet: wallet, - toAddress: accounts[5], - amount: 60, - data: 'abcde35f123', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: sequenceId - }; - - await expectSuccessfulSendMultiSig(params); - }); + it('Send with a sequence ID that has been previously used should fail', async function() { + sequenceId = usedSequenceId || (sequenceId - 1); + const params = { + msgSenderAddress: accounts[2], + otherSignerAddress: accounts[1], + wallet: wallet, + toAddress: accounts[5], + amount: 62, + data: '', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: sequenceId + }; + + await expectFailSendMultiSig(params); + }); - it('Can send with a sequence ID that is unused but lower than the previous (not strictly monotonic increase)', async function() { - sequenceId = 200; - const params = { - msgSenderAddress: accounts[0], - otherSignerAddress: accounts[1], - wallet: wallet, - toAddress: accounts[5], - amount: 61, - data: '100135f123', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: sequenceId - }; - - await expectSuccessfulSendMultiSig(params); - }); + it('Send with a sequence ID that is used many transactions ago (lower than previous 10) should fail', async function() { + sequenceId = 1; + const params = { + msgSenderAddress: accounts[0], + otherSignerAddress: accounts[1], + wallet: wallet, + toAddress: accounts[5], + amount: 63, + data: '5566abfe', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: sequenceId + }; + + await expectFailSendMultiSig(params); + }); - it('Send with a sequence ID that has been previously used should fail', async function() { - sequenceId = usedSequenceId || (sequenceId - 1); - const params = { - msgSenderAddress: accounts[2], - otherSignerAddress: accounts[1], - wallet: wallet, - toAddress: accounts[5], - amount: 62, - data: '', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: sequenceId - }; - - await expectFailSendMultiSig(params); + it('Sign with incorrect operation hash prefix should fail', async function() { + sequenceId = 1001; + const params = { + msgSenderAddress: accounts[0], + otherSignerAddress: accounts[1], + wallet: wallet, + toAddress: accounts[5], + amount: 63, + data: '5566abfe', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: sequenceId, + prefix: 'Invalid' + }; + + await expectFailSendMultiSig(params); + }); }); - it('Send with a sequence ID that is used many transactions ago (lower than previous 10) should fail', async function() { - sequenceId = 1; - const params = { - msgSenderAddress: accounts[0], - otherSignerAddress: accounts[1], - wallet: wallet, - toAddress: accounts[5], - amount: 63, - data: '5566abfe', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: sequenceId - }; - - await expectFailSendMultiSig(params); - }); - }); + describe('Safe mode', function() { + before(async function() { + // Create and fund the wallet + wallet = await WalletSimple.new([accounts[0], accounts[1], accounts[2]]); + web3.eth.sendTransaction({ from: accounts[0], to: wallet.address, value: web3.toWei(50000, 'ether') }); + }); - describe('Safe mode', function() { - before(async function() { - // Create and fund the wallet - wallet = await WalletSimple.new([accounts[0], accounts[1], accounts[2]]); - web3.eth.sendTransaction({ from: accounts[0], to: wallet.address, value: web3.toWei(50000, 'ether') }); - }); + it('Cannot be activated by unauthorized user', async function() { + try { + await wallet.activateSafeMode({ from: accounts[5] }); + throw new Error('should not be here'); + } catch(err) { + assertVMException(err); + } + const isSafeMode = await wallet.safeMode.call(); + isSafeMode.should.eql(false); + }); - it('Cannot be activated by unauthorized user', async function() { - try { - await wallet.activateSafeMode({ from: accounts[5] }); - throw new Error('should not be here'); - } catch(err) { - assertVMException(err); - } - const isSafeMode = await wallet.safeMode.call(); - isSafeMode.should.eql(false); - }); + it('Can be activated by any authorized signer', async function() { + for (let i=0; i<3; i++) { + const wallet = await WalletSimple.new([accounts[0], accounts[1], accounts[2]]); + await wallet.activateSafeMode({ from: accounts[i] }); + const isSafeMode = await wallet.safeMode.call(); + isSafeMode.should.eql(true); + } + }); - it('Can be activated by any authorized signer', async function() { - for (let i=0; i<3; i++) { - const wallet = await WalletSimple.new([accounts[0], accounts[1], accounts[2]]); - await wallet.activateSafeMode({ from: accounts[i] }); - const isSafeMode = await wallet.safeMode.call(); + it('Cannot send transactions to external addresses in safe mode', async function() { + let isSafeMode = await wallet.safeMode.call(); + isSafeMode.should.eql(false); + await wallet.activateSafeMode({ from: accounts[1] }); + isSafeMode = await wallet.safeMode.call(); isSafeMode.should.eql(true); - } - }); + await helpers.waitForEvents(walletEvents, 1); + const safeModeEvent = _.find(walletEvents, function(event) { + return event.event === 'SafeModeActivated'; + }); + should.exist(safeModeEvent); + safeModeEvent.args.msgSender.should.eql(accounts[1]); + + const params = { + msgSenderAddress: accounts[0], + otherSignerAddress: accounts[1], + wallet: wallet, + toAddress: accounts[8], + amount: 22, + data: '100135f123', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: 10001 + }; + + await expectFailSendMultiSig(params); + }); - it('Cannot send transactions to external addresses in safe mode', async function() { - let isSafeMode = await wallet.safeMode.call(); - isSafeMode.should.eql(false); - await wallet.activateSafeMode({ from: accounts[1] }); - isSafeMode = await wallet.safeMode.call(); - isSafeMode.should.eql(true); - await helpers.waitForEvents(walletEvents, 1); - const safeModeEvent = _.find(walletEvents, function(event) { - return event.event === 'SafeModeActivated'; - }); - should.exist(safeModeEvent); - safeModeEvent.args.msgSender.should.eql(accounts[1]); - - const params = { - msgSenderAddress: accounts[0], - otherSignerAddress: accounts[1], - wallet: wallet, - toAddress: accounts[8], - amount: 22, - data: '100135f123', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: 10001 - }; - - await expectFailSendMultiSig(params); + it('Can send transactions to signer addresses in safe mode', async function() { + const params = { + msgSenderAddress: accounts[2], + otherSignerAddress: accounts[1], + wallet: wallet, + toAddress: accounts[0], + amount: 28, + data: '100135f123', + expireTime: Math.floor((new Date().getTime()) / 1000) + 60, + sequenceId: 9000 + }; + + await expectSuccessfulSendMultiSig(params); + }); }); - it('Can send transactions to signer addresses in safe mode', async function() { - const params = { - msgSenderAddress: accounts[2], - otherSignerAddress: accounts[1], - wallet: wallet, - toAddress: accounts[0], - amount: 28, - data: '100135f123', - expireTime: Math.floor((new Date().getTime()) / 1000) + 60, - sequenceId: 9000 - }; - - await expectSuccessfulSendMultiSig(params); - }); - }); + describe('Forwarder addresses', function() { + const forwardAbi = [{ constant: false,inputs: [],name: 'flush',outputs: [],type: 'function' },{ constant: true,inputs: [],name: 'destinationAddress',outputs: [{ name: '',type: 'address' }],type: 'function' },{ inputs: [],type: 'constructor' }]; + const forwardContract = web3.eth.contract(forwardAbi); - describe('Forwarder addresses', function() { - const forwardAbi = [{ constant: false,inputs: [],name: 'flush',outputs: [],type: 'function' },{ constant: true,inputs: [],name: 'destinationAddress',outputs: [{ name: '',type: 'address' }],type: 'function' },{ inputs: [],type: 'constructor' }]; - const forwardContract = web3.eth.contract(forwardAbi); + it('Create and forward', async function() { + const wallet = await WalletSimple.new([accounts[0], accounts[1], accounts[2]]); + const forwarder = await createForwarderFromWallet(wallet); + web3.fromWei(web3.eth.getBalance(forwarder.address), 'ether').should.eql(web3.toBigNumber(0)); - it('Create and forward', async function() { - const wallet = await WalletSimple.new([accounts[0], accounts[1], accounts[2]]); - const forwarder = await createForwarderFromWallet(wallet); - web3.fromWei(web3.eth.getBalance(forwarder.address), 'ether').should.eql(web3.toBigNumber(0)); + web3.eth.sendTransaction({ from: accounts[1], to: forwarder.address, value: web3.toWei(200, 'ether') }); - web3.eth.sendTransaction({ from: accounts[1], to: forwarder.address, value: web3.toWei(200, 'ether') }); + // Verify funds forwarded + web3.fromWei(web3.eth.getBalance(forwarder.address), 'ether').should.eql(web3.toBigNumber(0)); + web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(200)); + }); - // Verify funds forwarded - web3.fromWei(web3.eth.getBalance(forwarder.address), 'ether').should.eql(web3.toBigNumber(0)); - web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(200)); - }); + it('Forwards value, not call data', async function () { + // When calling a nonexistent method on forwarder, transfer call value to target address and emit event on success. + // Don't call a method on target contract. + // + // While the WalletSimple contract has no side-effect methods that can be called from arbitrary msg.sender, + // this could change in the future. + // Simulate this with a ForwarderContract that has a side effect. + + const ForwarderTarget = artifacts.require('./ForwarderTarget.sol'); + + const forwarderTarget = await ForwarderTarget.new(); + // can be passed for wallet since it has the same interface + const forwarder = await createForwarderFromWallet(forwarderTarget); + const events = []; + forwarder.allEvents({}, (err, event) => { + if (err) { throw err; } + events.push(event); + }); + const forwarderAsTarget = ForwarderTarget.at(forwarder.address); + + const newData = 0xc0fefe; + + for (const setDataReturn of [true, false]) { + // clear events + events.length = 0; + + // calls without value emit deposited event but don't get forwarded + await forwarderAsTarget.setData(newData, setDataReturn); + (await forwarderTarget.data.call()).should.eql(web3.toBigNumber(0)); + + await helpers.waitForEvents(events, 1); + events.length.should.eql(1); + events.pop().event.should.eql('ForwarderDeposited'); + + // Same for setDataWithValue() + const oldBalance = web3.eth.getBalance(forwarderTarget.address); + await forwarderAsTarget.setDataWithValue(newData + 1, setDataReturn, { value: 100 }); + (await forwarderTarget.data.call()).should.eql(web3.toBigNumber(0)); + web3.eth.getBalance(forwarderTarget.address).should.eql(oldBalance.plus(100)); + + await helpers.waitForEvents(events, 1); + events.length.should.eql(1); + events.pop().event.should.eql('ForwarderDeposited'); + } + }); - it('Forwards value, not call data', async function () { - // When calling a nonexistent method on forwarder, transfer call value to target address and emit event on success. - // Don't call a method on target contract. - // - // While the WalletSimple contract has no side-effect methods that can be called from arbitrary msg.sender, - // this could change in the future. - // Simulate this with a ForwarderContract that has a side effect. - - const ForwarderTarget = artifacts.require('./ForwarderTarget.sol'); - - const forwarderTarget = await ForwarderTarget.new(); - // can be passed for wallet since it has the same interface - const forwarder = await createForwarderFromWallet(forwarderTarget); - const events = []; - forwarder.allEvents({}, (err, event) => { - if (err) { throw err; } - events.push(event); - }); - const forwarderAsTarget = ForwarderTarget.at(forwarder.address); - - const newData = 0xc0fefe; - - for (const setDataReturn of [true, false]) { - // clear events - events.length = 0; - - // calls without value emit deposited event but don't get forwarded - await forwarderAsTarget.setData(newData, setDataReturn); - (await forwarderTarget.data.call()).should.eql(web3.toBigNumber(0)); - - await helpers.waitForEvents(events, 1); - events.length.should.eql(1); - events.pop().event.should.eql('ForwarderDeposited'); - - // Same for setDataWithValue() - const oldBalance = web3.eth.getBalance(forwarderTarget.address); - await forwarderAsTarget.setDataWithValue(newData + 1, setDataReturn, { value: 100 }); - (await forwarderTarget.data.call()).should.eql(web3.toBigNumber(0)); - web3.eth.getBalance(forwarderTarget.address).should.eql(oldBalance.plus(100)); - - await helpers.waitForEvents(events, 1); - events.length.should.eql(1); - events.pop().event.should.eql('ForwarderDeposited'); - } - }); + it('Multiple forward contracts', async function() { + const numForwardAddresses = 10; + const etherEachSend = 4; + const wallet = await WalletSimple.new([accounts[2], accounts[3], accounts[4]]); + + // Create forwarders and send 4 ether to each of the addresses + for (let i=0; i < numForwardAddresses; i++) { + const forwarder = await createForwarderFromWallet(wallet); + web3.eth.sendTransaction({ from: accounts[1], to: forwarder.address, value: web3.toWei(etherEachSend, 'ether') }); + } + + // Verify all the forwarding is complete + web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(etherEachSend * numForwardAddresses)); + }); - it('Multiple forward contracts', async function() { - const numForwardAddresses = 10; - const etherEachSend = 4; - const wallet = await WalletSimple.new([accounts[2], accounts[3], accounts[4]]); + it('Send before create, then flush', async function() { + const wallet = await WalletSimple.new([accounts[3], accounts[4], accounts[5]]); + const forwarderContractAddress = helpers.getNextContractAddress(wallet.address); + web3.eth.sendTransaction({ from: accounts[1], to: forwarderContractAddress, value: web3.toWei(300, 'ether') }); + web3.fromWei(web3.eth.getBalance(forwarderContractAddress), 'ether').should.eql(web3.toBigNumber(300)); + web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(0)); - // Create forwarders and send 4 ether to each of the addresses - for (let i=0; i < numForwardAddresses; i++) { const forwarder = await createForwarderFromWallet(wallet); - web3.eth.sendTransaction({ from: accounts[1], to: forwarder.address, value: web3.toWei(etherEachSend, 'ether') }); - } + forwarder.address.should.eql(forwarderContractAddress); - // Verify all the forwarding is complete - web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(etherEachSend * numForwardAddresses)); - }); + // Verify that funds are still stuck in forwarder contract address + web3.fromWei(web3.eth.getBalance(forwarderContractAddress), 'ether').should.eql(web3.toBigNumber(300)); + web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(0)); - it('Send before create, then flush', async function() { - const wallet = await WalletSimple.new([accounts[3], accounts[4], accounts[5]]); - const forwarderContractAddress = helpers.getNextContractAddress(wallet.address); - web3.eth.sendTransaction({ from: accounts[1], to: forwarderContractAddress, value: web3.toWei(300, 'ether') }); - web3.fromWei(web3.eth.getBalance(forwarderContractAddress), 'ether').should.eql(web3.toBigNumber(300)); - web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(0)); + // Flush and verify + forwardContract.at(forwarderContractAddress).flush({ from: accounts[0] }); + web3.fromWei(web3.eth.getBalance(forwarderContractAddress), 'ether').should.eql(web3.toBigNumber(0)); + web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(300)); + }); + + it('Flush sent from external account', async function() { + const wallet = await WalletSimple.new([accounts[4], accounts[5], accounts[6]]); + const forwarderContractAddress = helpers.getNextContractAddress(wallet.address); + web3.eth.sendTransaction({ from: accounts[1], to: forwarderContractAddress, value: web3.toWei(300, 'ether') }); + web3.fromWei(web3.eth.getBalance(forwarderContractAddress), 'ether').should.eql(web3.toBigNumber(300)); + web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(0)); - const forwarder = await createForwarderFromWallet(wallet); - forwarder.address.should.eql(forwarderContractAddress); + const forwarder = await createForwarderFromWallet(wallet); + forwarder.address.should.eql(forwarderContractAddress); - // Verify that funds are still stuck in forwarder contract address - web3.fromWei(web3.eth.getBalance(forwarderContractAddress), 'ether').should.eql(web3.toBigNumber(300)); - web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(0)); + // Verify that funds are still stuck in forwarder contract address + web3.fromWei(web3.eth.getBalance(forwarder.address), 'ether').should.eql(web3.toBigNumber(300)); + web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(0)); - // Flush and verify - forwardContract.at(forwarderContractAddress).flush({ from: accounts[0] }); - web3.fromWei(web3.eth.getBalance(forwarderContractAddress), 'ether').should.eql(web3.toBigNumber(0)); - web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(300)); + // Flush and verify + forwardContract.at(forwarder.address).flush({ from: accounts[0] }); + web3.fromWei(web3.eth.getBalance(forwarder.address), 'ether').should.eql(web3.toBigNumber(0)); + web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(300)); + }); }); - it('Flush sent from external account', async function() { - const wallet = await WalletSimple.new([accounts[4], accounts[5], accounts[6]]); - const forwarderContractAddress = helpers.getNextContractAddress(wallet.address); - web3.eth.sendTransaction({ from: accounts[1], to: forwarderContractAddress, value: web3.toWei(300, 'ether') }); - web3.fromWei(web3.eth.getBalance(forwarderContractAddress), 'ether').should.eql(web3.toBigNumber(300)); - web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(0)); + describe('ERC20 token transfers', function() { + let fixedSupplyTokenContract; + before(async function() { + // Create and fund the wallet + wallet = await WalletSimple.new([accounts[4], accounts[5], accounts[6]]); + fixedSupplyTokenContract = await FixedSupplyToken.new(undefined, { from: accounts[0] }); + const balance = await fixedSupplyTokenContract.balanceOf.call(accounts[0]); + balance.should.eql(web3.toBigNumber(1000000)); + }); - const forwarder = await createForwarderFromWallet(wallet); - forwarder.address.should.eql(forwarderContractAddress); + it('Receive and Send tokens from main wallet contract', async function() { - // Verify that funds are still stuck in forwarder contract address - web3.fromWei(web3.eth.getBalance(forwarder.address), 'ether').should.eql(web3.toBigNumber(300)); - web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(0)); + await fixedSupplyTokenContract.transfer(wallet.address, 100, { from: accounts[0] }); + const balance = await fixedSupplyTokenContract.balanceOf.call(accounts[0]); + balance.should.eql(web3.toBigNumber(1000000 - 100)); + const msigWalletStartTokens = await fixedSupplyTokenContract.balanceOf.call(wallet.address); + msigWalletStartTokens.should.eql(web3.toBigNumber(100)); - // Flush and verify - forwardContract.at(forwarder.address).flush({ from: accounts[0] }); - web3.fromWei(web3.eth.getBalance(forwarder.address), 'ether').should.eql(web3.toBigNumber(0)); - web3.fromWei(web3.eth.getBalance(wallet.address), 'ether').should.eql(web3.toBigNumber(300)); - }); - }); + const sequenceIdString = await wallet.getNextSequenceId.call(); + const sequenceId = parseInt(sequenceIdString); - describe('ERC20 token transfers', function() { - let fixedSupplyTokenContract; - before(async function() { - // Create and fund the wallet - wallet = await WalletSimple.new([accounts[4], accounts[5], accounts[6]]); - fixedSupplyTokenContract = await FixedSupplyToken.new(undefined, { from: accounts[0] }); - const balance = await fixedSupplyTokenContract.balanceOf.call(accounts[0]); - balance.should.eql(web3.toBigNumber(1000000)); - }); + const destinationAccount = accounts[5]; + const amount = 50; + const expireTime = Math.floor((new Date().getTime()) / 1000) + 60; // 60 seconds - it('Receive and Send tokens from main wallet contract', async function() { - - await fixedSupplyTokenContract.transfer(wallet.address, 100, { from: accounts[0] }); - const balance = await fixedSupplyTokenContract.balanceOf.call(accounts[0]); - balance.should.eql(web3.toBigNumber(1000000 - 100)); - const msigWalletStartTokens = await fixedSupplyTokenContract.balanceOf.call(wallet.address); - msigWalletStartTokens.should.eql(web3.toBigNumber(100)); - - const sequenceIdString = await wallet.getNextSequenceId.call(); - const sequenceId = parseInt(sequenceIdString); + const destinationAccountStartTokens = await fixedSupplyTokenContract.balanceOf.call(accounts[5]); + destinationAccountStartTokens.should.eql(web3.toBigNumber(0)); - const destinationAccount = accounts[5]; - const amount = 50; - const expireTime = Math.floor((new Date().getTime()) / 1000) + 60; // 60 seconds + const operationHash = helpers.getSha3ForConfirmationTokenTx( + tokenPrefix, destinationAccount, amount, fixedSupplyTokenContract.address, expireTime, sequenceId + ); + const sig = util.ecsign(operationHash, privateKeyForAccount(accounts[4])); - const destinationAccountStartTokens = await fixedSupplyTokenContract.balanceOf.call(accounts[5]); - destinationAccountStartTokens.should.eql(web3.toBigNumber(0)); + await wallet.sendMultiSigToken( + destinationAccount, amount, fixedSupplyTokenContract.address, expireTime, sequenceId, helpers.serializeSignature(sig), + { from: accounts[5] } + ); + const destinationAccountEndTokens = await fixedSupplyTokenContract.balanceOf.call(destinationAccount); + destinationAccountStartTokens.plus(amount).should.eql(destinationAccountEndTokens); - const operationHash = helpers.getSha3ForConfirmationTokenTx( - destinationAccount, amount, fixedSupplyTokenContract.address, expireTime, sequenceId - ); - const sig = util.ecsign(operationHash, privateKeyForAccount(accounts[4])); + // Check wallet balance + const msigWalletEndTokens = await fixedSupplyTokenContract.balanceOf.call(wallet.address); + msigWalletStartTokens.minus(amount).should.eql(msigWalletEndTokens); + }); - await wallet.sendMultiSigToken( - destinationAccount, amount, fixedSupplyTokenContract.address, expireTime, sequenceId, helpers.serializeSignature(sig), - { from: accounts[5] } - ); - const destinationAccountEndTokens = await fixedSupplyTokenContract.balanceOf.call(destinationAccount); - destinationAccountStartTokens.plus(amount).should.eql(destinationAccountEndTokens); + it('Flush from Forwarder contract', async function() { + const forwarder = await createForwarderFromWallet(wallet); + await fixedSupplyTokenContract.transfer(forwarder.address, 100, { from: accounts[0] }); + const balance = await fixedSupplyTokenContract.balanceOf.call(accounts[0]); + balance.should.eql(web3.toBigNumber(1000000 - 100 - 100)); - // Check wallet balance - const msigWalletEndTokens = await fixedSupplyTokenContract.balanceOf.call(wallet.address); - msigWalletStartTokens.minus(amount).should.eql(msigWalletEndTokens); - }); + const forwarderContractStartTokens = await fixedSupplyTokenContract.balanceOf.call(forwarder.address); + forwarderContractStartTokens.should.eql(web3.toBigNumber(100)); + const walletContractStartTokens = await fixedSupplyTokenContract.balanceOf.call(wallet.address); + + await wallet.flushForwarderTokens(forwarder.address, fixedSupplyTokenContract.address, { from: accounts[5] }); + const forwarderAccountEndTokens = await fixedSupplyTokenContract.balanceOf.call(forwarder.address); + forwarderAccountEndTokens.should.eql(web3.toBigNumber(0)); + + // Check wallet balance + const walletContractEndTokens = await fixedSupplyTokenContract.balanceOf.call(wallet.address); + walletContractStartTokens.plus(100).should.eql(walletContractEndTokens); + /* TODO Barath - Get event testing for forwarder contract token send to work + */ + }); - it('Flush from Forwarder contract', async function() { - const forwarder = await createForwarderFromWallet(wallet); - await fixedSupplyTokenContract.transfer(forwarder.address, 100, { from: accounts[0] }); - const balance = await fixedSupplyTokenContract.balanceOf.call(accounts[0]); - balance.should.eql(web3.toBigNumber(1000000 - 100 - 100)); - - const forwarderContractStartTokens = await fixedSupplyTokenContract.balanceOf.call(forwarder.address); - forwarderContractStartTokens.should.eql(web3.toBigNumber(100)); - const walletContractStartTokens = await fixedSupplyTokenContract.balanceOf.call(wallet.address); - - await wallet.flushForwarderTokens(forwarder.address, fixedSupplyTokenContract.address, { from: accounts[5] }); - const forwarderAccountEndTokens = await fixedSupplyTokenContract.balanceOf.call(forwarder.address); - forwarderAccountEndTokens.should.eql(web3.toBigNumber(0)); - - // Check wallet balance - const walletContractEndTokens = await fixedSupplyTokenContract.balanceOf.call(wallet.address); - walletContractStartTokens.plus(100).should.eql(walletContractEndTokens); - /* TODO Barath - Get event testing for forwarder contract token send to work - */ }); }); -}); + +});