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 - */ }); }); -}); + +});