aboutsummaryrefslogtreecommitdiffstats
path: root/contracts/multisig
diff options
context:
space:
mode:
Diffstat (limited to 'contracts/multisig')
-rw-r--r--contracts/multisig/.solhint.json20
-rw-r--r--contracts/multisig/CHANGELOG.json1
-rw-r--r--contracts/multisig/README.md70
-rw-r--r--contracts/multisig/compiler.json22
-rw-r--r--contracts/multisig/contracts/multisig/MultiSigWallet.sol393
-rw-r--r--contracts/multisig/contracts/multisig/MultiSigWalletWithTimeLock.sol127
-rw-r--r--contracts/multisig/contracts/test/TestRejectEther/TestRejectEther.sol23
-rw-r--r--contracts/multisig/package.json86
-rw-r--r--contracts/multisig/src/artifacts/index.ts11
-rw-r--r--contracts/multisig/src/wrappers/index.ts2
-rw-r--r--contracts/multisig/test/global_hooks.ts19
-rw-r--r--contracts/multisig/test/multi_sig_with_time_lock.ts349
-rw-r--r--contracts/multisig/test/utils/multi_sig_wrapper.ts54
-rw-r--r--contracts/multisig/tsconfig.json15
-rw-r--r--contracts/multisig/tslint.json6
15 files changed, 1198 insertions, 0 deletions
diff --git a/contracts/multisig/.solhint.json b/contracts/multisig/.solhint.json
new file mode 100644
index 000000000..076afe9f3
--- /dev/null
+++ b/contracts/multisig/.solhint.json
@@ -0,0 +1,20 @@
+{
+ "extends": "default",
+ "rules": {
+ "avoid-low-level-calls": false,
+ "avoid-tx-origin": "warn",
+ "bracket-align": false,
+ "code-complexity": false,
+ "const-name-snakecase": "error",
+ "expression-indent": "error",
+ "function-max-lines": false,
+ "func-order": "error",
+ "indent": ["error", 4],
+ "max-line-length": ["warn", 160],
+ "no-inline-assembly": false,
+ "quotes": ["error", "double"],
+ "separate-by-one-line-in-contract": "error",
+ "space-after-comma": "error",
+ "statement-indent": "error"
+ }
+}
diff --git a/contracts/multisig/CHANGELOG.json b/contracts/multisig/CHANGELOG.json
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/contracts/multisig/CHANGELOG.json
@@ -0,0 +1 @@
+[]
diff --git a/contracts/multisig/README.md b/contracts/multisig/README.md
new file mode 100644
index 000000000..93db63b5b
--- /dev/null
+++ b/contracts/multisig/README.md
@@ -0,0 +1,70 @@
+## MultisSig Contracts
+
+MultiSig smart contracts
+
+## Usage
+
+Contracts can be found in the [contracts](./contracts) directory. The contents of this directory are broken down into the following subdirectories:
+
+* [multisig](./contracts/multisig)
+ * This directory contains the [Gnosis MultiSigWallet](https://github.com/gnosis/MultiSigWallet) and a custom extension that adds a timelock to transactions within the MultiSigWallet.
+* [test](./contracts/test)
+ * This directory contains mocks and other contracts that are used solely for testing contracts within the other directories.
+
+## Contributing
+
+We strongly recommend that the community help us make improvements and determine the future direction of the protocol. To report bugs within this package, please create an issue in this repository.
+
+For proposals regarding the 0x protocol's smart contract architecture, message format, or additional functionality, go to the [0x Improvement Proposals (ZEIPs)](https://github.com/0xProject/ZEIPs) repository and follow the contribution guidelines provided therein.
+
+Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started.
+
+### Install Dependencies
+
+If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them:
+
+```bash
+yarn config set workspaces-experimental true
+```
+
+Then install dependencies
+
+```bash
+yarn install
+```
+
+### Build
+
+To build this package and all other monorepo packages that it depends on, run the following from the monorepo root directory:
+
+```bash
+PKG=@0x/contracts-multisig yarn build
+```
+
+Or continuously rebuild on change:
+
+```bash
+PKG=@0x/contracts-multisig yarn watch
+```
+
+### Clean
+
+```bash
+yarn clean
+```
+
+### Lint
+
+```bash
+yarn lint
+```
+
+### Run Tests
+
+```bash
+yarn test
+```
+
+#### Testing options
+
+Contracts testing options like coverage, profiling, revert traces or backing node choosing - are described [here](../TESTING.md).
diff --git a/contracts/multisig/compiler.json b/contracts/multisig/compiler.json
new file mode 100644
index 000000000..5a1f689e2
--- /dev/null
+++ b/contracts/multisig/compiler.json
@@ -0,0 +1,22 @@
+{
+ "artifactsDir": "./generated-artifacts",
+ "contractsDir": "./contracts",
+ "compilerSettings": {
+ "optimizer": {
+ "enabled": true,
+ "runs": 1000000
+ },
+ "outputSelection": {
+ "*": {
+ "*": [
+ "abi",
+ "evm.bytecode.object",
+ "evm.bytecode.sourceMap",
+ "evm.deployedBytecode.object",
+ "evm.deployedBytecode.sourceMap"
+ ]
+ }
+ }
+ },
+ "contracts": ["MultiSigWallet", "MultiSigWalletWithTimeLock", "TestRejectEther"]
+}
diff --git a/contracts/multisig/contracts/multisig/MultiSigWallet.sol b/contracts/multisig/contracts/multisig/MultiSigWallet.sol
new file mode 100644
index 000000000..516e7391c
--- /dev/null
+++ b/contracts/multisig/contracts/multisig/MultiSigWallet.sol
@@ -0,0 +1,393 @@
+// solhint-disable
+pragma solidity ^0.4.15;
+
+
+/// @title Multisignature wallet - Allows multiple parties to agree on transactions before execution.
+/// @author Stefan George - <stefan.george@consensys.net>
+contract MultiSigWallet {
+
+ /*
+ * Events
+ */
+ event Confirmation(address indexed sender, uint indexed transactionId);
+ event Revocation(address indexed sender, uint indexed transactionId);
+ event Submission(uint indexed transactionId);
+ event Execution(uint indexed transactionId);
+ event ExecutionFailure(uint indexed transactionId);
+ event Deposit(address indexed sender, uint value);
+ event OwnerAddition(address indexed owner);
+ event OwnerRemoval(address indexed owner);
+ event RequirementChange(uint required);
+
+ /*
+ * Constants
+ */
+ uint constant public MAX_OWNER_COUNT = 50;
+
+ /*
+ * Storage
+ */
+ mapping (uint => Transaction) public transactions;
+ mapping (uint => mapping (address => bool)) public confirmations;
+ mapping (address => bool) public isOwner;
+ address[] public owners;
+ uint public required;
+ uint public transactionCount;
+
+ struct Transaction {
+ address destination;
+ uint value;
+ bytes data;
+ bool executed;
+ }
+
+ /*
+ * Modifiers
+ */
+ modifier onlyWallet() {
+ require(msg.sender == address(this));
+ _;
+ }
+
+ modifier ownerDoesNotExist(address owner) {
+ require(!isOwner[owner]);
+ _;
+ }
+
+ modifier ownerExists(address owner) {
+ require(isOwner[owner]);
+ _;
+ }
+
+ modifier transactionExists(uint transactionId) {
+ require(transactions[transactionId].destination != 0);
+ _;
+ }
+
+ modifier confirmed(uint transactionId, address owner) {
+ require(confirmations[transactionId][owner]);
+ _;
+ }
+
+ modifier notConfirmed(uint transactionId, address owner) {
+ require(!confirmations[transactionId][owner]);
+ _;
+ }
+
+ modifier notExecuted(uint transactionId) {
+ require(!transactions[transactionId].executed);
+ _;
+ }
+
+ modifier notNull(address _address) {
+ require(_address != 0);
+ _;
+ }
+
+ modifier validRequirement(uint ownerCount, uint _required) {
+ require(ownerCount <= MAX_OWNER_COUNT
+ && _required <= ownerCount
+ && _required != 0
+ && ownerCount != 0);
+ _;
+ }
+
+ /// @dev Fallback function allows to deposit ether.
+ function()
+ payable
+ {
+ if (msg.value > 0)
+ Deposit(msg.sender, msg.value);
+ }
+
+ /*
+ * Public functions
+ */
+ /// @dev Contract constructor sets initial owners and required number of confirmations.
+ /// @param _owners List of initial owners.
+ /// @param _required Number of required confirmations.
+ function MultiSigWallet(address[] _owners, uint _required)
+ public
+ validRequirement(_owners.length, _required)
+ {
+ for (uint i=0; i<_owners.length; i++) {
+ require(!isOwner[_owners[i]] && _owners[i] != 0);
+ isOwner[_owners[i]] = true;
+ }
+ owners = _owners;
+ required = _required;
+ }
+
+ /// @dev Allows to add a new owner. Transaction has to be sent by wallet.
+ /// @param owner Address of new owner.
+ function addOwner(address owner)
+ public
+ onlyWallet
+ ownerDoesNotExist(owner)
+ notNull(owner)
+ validRequirement(owners.length + 1, required)
+ {
+ isOwner[owner] = true;
+ owners.push(owner);
+ OwnerAddition(owner);
+ }
+
+ /// @dev Allows to remove an owner. Transaction has to be sent by wallet.
+ /// @param owner Address of owner.
+ function removeOwner(address owner)
+ public
+ onlyWallet
+ ownerExists(owner)
+ {
+ isOwner[owner] = false;
+ for (uint i=0; i<owners.length - 1; i++)
+ if (owners[i] == owner) {
+ owners[i] = owners[owners.length - 1];
+ break;
+ }
+ owners.length -= 1;
+ if (required > owners.length)
+ changeRequirement(owners.length);
+ OwnerRemoval(owner);
+ }
+
+ /// @dev Allows to replace an owner with a new owner. Transaction has to be sent by wallet.
+ /// @param owner Address of owner to be replaced.
+ /// @param newOwner Address of new owner.
+ function replaceOwner(address owner, address newOwner)
+ public
+ onlyWallet
+ ownerExists(owner)
+ ownerDoesNotExist(newOwner)
+ {
+ for (uint i=0; i<owners.length; i++)
+ if (owners[i] == owner) {
+ owners[i] = newOwner;
+ break;
+ }
+ isOwner[owner] = false;
+ isOwner[newOwner] = true;
+ OwnerRemoval(owner);
+ OwnerAddition(newOwner);
+ }
+
+ /// @dev Allows to change the number of required confirmations. Transaction has to be sent by wallet.
+ /// @param _required Number of required confirmations.
+ function changeRequirement(uint _required)
+ public
+ onlyWallet
+ validRequirement(owners.length, _required)
+ {
+ required = _required;
+ RequirementChange(_required);
+ }
+
+ /// @dev Allows an owner to submit and confirm a transaction.
+ /// @param destination Transaction target address.
+ /// @param value Transaction ether value.
+ /// @param data Transaction data payload.
+ /// @return Returns transaction ID.
+ function submitTransaction(address destination, uint value, bytes data)
+ public
+ returns (uint transactionId)
+ {
+ transactionId = addTransaction(destination, value, data);
+ confirmTransaction(transactionId);
+ }
+
+ /// @dev Allows an owner to confirm a transaction.
+ /// @param transactionId Transaction ID.
+ function confirmTransaction(uint transactionId)
+ public
+ ownerExists(msg.sender)
+ transactionExists(transactionId)
+ notConfirmed(transactionId, msg.sender)
+ {
+ confirmations[transactionId][msg.sender] = true;
+ Confirmation(msg.sender, transactionId);
+ executeTransaction(transactionId);
+ }
+
+ /// @dev Allows an owner to revoke a confirmation for a transaction.
+ /// @param transactionId Transaction ID.
+ function revokeConfirmation(uint transactionId)
+ public
+ ownerExists(msg.sender)
+ confirmed(transactionId, msg.sender)
+ notExecuted(transactionId)
+ {
+ confirmations[transactionId][msg.sender] = false;
+ Revocation(msg.sender, transactionId);
+ }
+
+ /// @dev Allows anyone to execute a confirmed transaction.
+ /// @param transactionId Transaction ID.
+ function executeTransaction(uint transactionId)
+ public
+ ownerExists(msg.sender)
+ confirmed(transactionId, msg.sender)
+ notExecuted(transactionId)
+ {
+ if (isConfirmed(transactionId)) {
+ Transaction storage txn = transactions[transactionId];
+ txn.executed = true;
+ if (external_call(txn.destination, txn.value, txn.data.length, txn.data))
+ Execution(transactionId);
+ else {
+ ExecutionFailure(transactionId);
+ txn.executed = false;
+ }
+ }
+ }
+
+ // call has been separated into its own function in order to take advantage
+ // of the Solidity's code generator to produce a loop that copies tx.data into memory.
+ function external_call(address destination, uint value, uint dataLength, bytes data) internal returns (bool) {
+ bool result;
+ assembly {
+ let x := mload(0x40) // "Allocate" memory for output (0x40 is where "free memory" pointer is stored by convention)
+ let d := add(data, 32) // First 32 bytes are the padded length of data, so exclude that
+ result := call(
+ sub(gas, 34710), // 34710 is the value that solidity is currently emitting
+ // It includes callGas (700) + callVeryLow (3, to pay for SUB) + callValueTransferGas (9000) +
+ // callNewAccountGas (25000, in case the destination address does not exist and needs creating)
+ destination,
+ value,
+ d,
+ dataLength, // Size of the input (in bytes) - this is what fixes the padding problem
+ x,
+ 0 // Output is ignored, therefore the output size is zero
+ )
+ }
+ return result;
+ }
+
+ /// @dev Returns the confirmation status of a transaction.
+ /// @param transactionId Transaction ID.
+ /// @return Confirmation status.
+ function isConfirmed(uint transactionId)
+ public
+ constant
+ returns (bool)
+ {
+ uint count = 0;
+ for (uint i=0; i<owners.length; i++) {
+ if (confirmations[transactionId][owners[i]])
+ count += 1;
+ if (count == required)
+ return true;
+ }
+ }
+
+ /*
+ * Internal functions
+ */
+ /// @dev Adds a new transaction to the transaction mapping, if transaction does not exist yet.
+ /// @param destination Transaction target address.
+ /// @param value Transaction ether value.
+ /// @param data Transaction data payload.
+ /// @return Returns transaction ID.
+ function addTransaction(address destination, uint value, bytes data)
+ internal
+ notNull(destination)
+ returns (uint transactionId)
+ {
+ transactionId = transactionCount;
+ transactions[transactionId] = Transaction({
+ destination: destination,
+ value: value,
+ data: data,
+ executed: false
+ });
+ transactionCount += 1;
+ Submission(transactionId);
+ }
+
+ /*
+ * Web3 call functions
+ */
+ /// @dev Returns number of confirmations of a transaction.
+ /// @param transactionId Transaction ID.
+ /// @return Number of confirmations.
+ function getConfirmationCount(uint transactionId)
+ public
+ constant
+ returns (uint count)
+ {
+ for (uint i=0; i<owners.length; i++)
+ if (confirmations[transactionId][owners[i]])
+ count += 1;
+ }
+
+ /// @dev Returns total number of transactions after filers are applied.
+ /// @param pending Include pending transactions.
+ /// @param executed Include executed transactions.
+ /// @return Total number of transactions after filters are applied.
+ function getTransactionCount(bool pending, bool executed)
+ public
+ constant
+ returns (uint count)
+ {
+ for (uint i=0; i<transactionCount; i++)
+ if ( pending && !transactions[i].executed
+ || executed && transactions[i].executed)
+ count += 1;
+ }
+
+ /// @dev Returns list of owners.
+ /// @return List of owner addresses.
+ function getOwners()
+ public
+ constant
+ returns (address[])
+ {
+ return owners;
+ }
+
+ /// @dev Returns array with owner addresses, which confirmed transaction.
+ /// @param transactionId Transaction ID.
+ /// @return Returns array of owner addresses.
+ function getConfirmations(uint transactionId)
+ public
+ constant
+ returns (address[] _confirmations)
+ {
+ address[] memory confirmationsTemp = new address[](owners.length);
+ uint count = 0;
+ uint i;
+ for (i=0; i<owners.length; i++)
+ if (confirmations[transactionId][owners[i]]) {
+ confirmationsTemp[count] = owners[i];
+ count += 1;
+ }
+ _confirmations = new address[](count);
+ for (i=0; i<count; i++)
+ _confirmations[i] = confirmationsTemp[i];
+ }
+
+ /// @dev Returns list of transaction IDs in defined range.
+ /// @param from Index start position of transaction array.
+ /// @param to Index end position of transaction array.
+ /// @param pending Include pending transactions.
+ /// @param executed Include executed transactions.
+ /// @return Returns array of transaction IDs.
+ function getTransactionIds(uint from, uint to, bool pending, bool executed)
+ public
+ constant
+ returns (uint[] _transactionIds)
+ {
+ uint[] memory transactionIdsTemp = new uint[](transactionCount);
+ uint count = 0;
+ uint i;
+ for (i=0; i<transactionCount; i++)
+ if ( pending && !transactions[i].executed
+ || executed && transactions[i].executed)
+ {
+ transactionIdsTemp[count] = i;
+ count += 1;
+ }
+ _transactionIds = new uint[](to - from);
+ for (i=from; i<to; i++)
+ _transactionIds[i - from] = transactionIdsTemp[i];
+ }
+} \ No newline at end of file
diff --git a/contracts/multisig/contracts/multisig/MultiSigWalletWithTimeLock.sol b/contracts/multisig/contracts/multisig/MultiSigWalletWithTimeLock.sol
new file mode 100644
index 000000000..9513d3b30
--- /dev/null
+++ b/contracts/multisig/contracts/multisig/MultiSigWalletWithTimeLock.sol
@@ -0,0 +1,127 @@
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+
+import "./MultiSigWallet.sol";
+
+
+/// @title Multisignature wallet with time lock- Allows multiple parties to execute a transaction after a time lock has passed.
+/// @author Amir Bandeali - <amir@0xProject.com>
+// solhint-disable not-rely-on-time
+contract MultiSigWalletWithTimeLock is
+ MultiSigWallet
+{
+ event ConfirmationTimeSet(uint256 indexed transactionId, uint256 confirmationTime);
+ event TimeLockChange(uint256 secondsTimeLocked);
+
+ uint256 public secondsTimeLocked;
+
+ mapping (uint256 => uint256) public confirmationTimes;
+
+ modifier notFullyConfirmed(uint256 transactionId) {
+ require(
+ !isConfirmed(transactionId),
+ "TX_FULLY_CONFIRMED"
+ );
+ _;
+ }
+
+ modifier fullyConfirmed(uint256 transactionId) {
+ require(
+ isConfirmed(transactionId),
+ "TX_NOT_FULLY_CONFIRMED"
+ );
+ _;
+ }
+
+ modifier pastTimeLock(uint256 transactionId) {
+ require(
+ block.timestamp >= confirmationTimes[transactionId] + secondsTimeLocked,
+ "TIME_LOCK_INCOMPLETE"
+ );
+ _;
+ }
+
+ /// @dev Contract constructor sets initial owners, required number of confirmations, and time lock.
+ /// @param _owners List of initial owners.
+ /// @param _required Number of required confirmations.
+ /// @param _secondsTimeLocked Duration needed after a transaction is confirmed and before it becomes executable, in seconds.
+ constructor (
+ address[] _owners,
+ uint256 _required,
+ uint256 _secondsTimeLocked
+ )
+ public
+ MultiSigWallet(_owners, _required)
+ {
+ secondsTimeLocked = _secondsTimeLocked;
+ }
+
+ /// @dev Changes the duration of the time lock for transactions.
+ /// @param _secondsTimeLocked Duration needed after a transaction is confirmed and before it becomes executable, in seconds.
+ function changeTimeLock(uint256 _secondsTimeLocked)
+ public
+ onlyWallet
+ {
+ secondsTimeLocked = _secondsTimeLocked;
+ emit TimeLockChange(_secondsTimeLocked);
+ }
+
+ /// @dev Allows an owner to confirm a transaction.
+ /// @param transactionId Transaction ID.
+ function confirmTransaction(uint256 transactionId)
+ public
+ ownerExists(msg.sender)
+ transactionExists(transactionId)
+ notConfirmed(transactionId, msg.sender)
+ notFullyConfirmed(transactionId)
+ {
+ confirmations[transactionId][msg.sender] = true;
+ emit Confirmation(msg.sender, transactionId);
+ if (isConfirmed(transactionId)) {
+ setConfirmationTime(transactionId, block.timestamp);
+ }
+ }
+
+ /// @dev Allows anyone to execute a confirmed transaction.
+ /// @param transactionId Transaction ID.
+ function executeTransaction(uint256 transactionId)
+ public
+ notExecuted(transactionId)
+ fullyConfirmed(transactionId)
+ pastTimeLock(transactionId)
+ {
+ Transaction storage txn = transactions[transactionId];
+ txn.executed = true;
+ if (external_call(txn.destination, txn.value, txn.data.length, txn.data)) {
+ emit Execution(transactionId);
+ } else {
+ emit ExecutionFailure(transactionId);
+ txn.executed = false;
+ }
+ }
+
+ /// @dev Sets the time of when a submission first passed.
+ function setConfirmationTime(uint256 transactionId, uint256 confirmationTime)
+ internal
+ {
+ confirmationTimes[transactionId] = confirmationTime;
+ emit ConfirmationTimeSet(transactionId, confirmationTime);
+ }
+}
diff --git a/contracts/multisig/contracts/test/TestRejectEther/TestRejectEther.sol b/contracts/multisig/contracts/test/TestRejectEther/TestRejectEther.sol
new file mode 100644
index 000000000..e523f591d
--- /dev/null
+++ b/contracts/multisig/contracts/test/TestRejectEther/TestRejectEther.sol
@@ -0,0 +1,23 @@
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+
+
+// solhint-disable no-empty-blocks
+contract TestRejectEther {}
diff --git a/contracts/multisig/package.json b/contracts/multisig/package.json
new file mode 100644
index 000000000..37d064fef
--- /dev/null
+++ b/contracts/multisig/package.json
@@ -0,0 +1,86 @@
+{
+ "private": true,
+ "name": "@0x/contracts-multisig",
+ "version": "1.0.0",
+ "engines": {
+ "node": ">=6.12"
+ },
+ "description": "Multisig contracts used by 0x protocol",
+ "main": "lib/src/index.js",
+ "directories": {
+ "test": "test"
+ },
+ "scripts": {
+ "build": "yarn pre_build && tsc -b",
+ "build:ci": "yarn build",
+ "pre_build": "run-s compile generate_contract_wrappers",
+ "test": "yarn run_mocha",
+ "rebuild_and_test": "run-s build test",
+ "test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov",
+ "test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html",
+ "test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha",
+ "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit",
+ "compile": "sol-compiler --contracts-dir contracts",
+ "clean": "shx rm -rf lib generated-artifacts generated-wrappers",
+ "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../packages/abi-gen-templates/contract.handlebars --partials '../../packages/abi-gen-templates/partials/**/*.handlebars' --output generated-wrappers --backend ethers",
+ "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts",
+ "coverage:report:text": "istanbul report text",
+ "coverage:report:html": "istanbul report html && open coverage/index.html",
+ "profiler:report:html": "istanbul report html && open coverage/index.html",
+ "coverage:report:lcov": "istanbul report lcov",
+ "test:circleci": "yarn test",
+ "lint-contracts": "solhint contracts/**/**/**/**/*.sol"
+ },
+ "config": {
+ "abis": "generated-artifacts/@(MultiSigWallet|MultiSigWalletWithTimeLock|TestRejectEther).json"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/0xProject/0x-monorepo.git"
+ },
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/0xProject/0x-monorepo/issues"
+ },
+ "homepage": "https://github.com/0xProject/0x-monorepo/contracts/core/README.md",
+ "devDependencies": {
+ "@0x/contracts-test-utils": "^1.0.0",
+ "@0x/abi-gen": "^1.0.17",
+ "@0x/dev-utils": "^1.0.18",
+ "@0x/sol-compiler": "^1.1.13",
+ "@0x/sol-cov": "^2.1.13",
+ "@0x/subproviders": "^2.1.5",
+ "@0x/tslint-config": "^1.0.10",
+ "@types/bn.js": "^4.11.0",
+ "@types/ethereumjs-abi": "^0.6.0",
+ "@types/lodash": "4.14.104",
+ "@types/node": "*",
+ "@types/yargs": "^10.0.0",
+ "chai": "^4.0.1",
+ "chai-as-promised": "^7.1.0",
+ "chai-bignumber": "^2.0.1",
+ "dirty-chai": "^2.0.1",
+ "make-promises-safe": "^1.1.0",
+ "mocha": "^4.1.0",
+ "npm-run-all": "^4.1.2",
+ "shx": "^0.2.2",
+ "solc": "^0.4.24",
+ "solhint": "^1.2.1",
+ "tslint": "5.11.0",
+ "typescript": "3.0.1",
+ "yargs": "^10.0.3"
+ },
+ "dependencies": {
+ "@0x/base-contract": "^3.0.7",
+ "@0x/order-utils": "^3.0.3",
+ "@0x/types": "^1.3.0",
+ "@0x/typescript-typings": "^3.0.4",
+ "@0x/utils": "^2.0.6",
+ "@0x/web3-wrapper": "^3.1.5",
+ "ethereum-types": "^1.1.2",
+ "lodash": "^4.17.5"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/contracts/multisig/src/artifacts/index.ts b/contracts/multisig/src/artifacts/index.ts
new file mode 100644
index 000000000..7cf47be01
--- /dev/null
+++ b/contracts/multisig/src/artifacts/index.ts
@@ -0,0 +1,11 @@
+import { ContractArtifact } from 'ethereum-types';
+
+import * as MultiSigWallet from '../../generated-artifacts/MultiSigWallet.json';
+import * as MultiSigWalletWithTimeLock from '../../generated-artifacts/MultiSigWalletWithTimeLock.json';
+import * as TestRejectEther from '../../generated-artifacts/TestRejectEther.json';
+
+export const artifacts = {
+ TestRejectEther: TestRejectEther as ContractArtifact,
+ MultiSigWallet: MultiSigWallet as ContractArtifact,
+ MultiSigWalletWithTimeLock: MultiSigWalletWithTimeLock as ContractArtifact,
+};
diff --git a/contracts/multisig/src/wrappers/index.ts b/contracts/multisig/src/wrappers/index.ts
new file mode 100644
index 000000000..69abd62f2
--- /dev/null
+++ b/contracts/multisig/src/wrappers/index.ts
@@ -0,0 +1,2 @@
+export * from '../../generated-wrappers/multi_sig_wallet';
+export * from '../../generated-wrappers/multi_sig_wallet_with_time_lock';
diff --git a/contracts/multisig/test/global_hooks.ts b/contracts/multisig/test/global_hooks.ts
new file mode 100644
index 000000000..68eb4f8d5
--- /dev/null
+++ b/contracts/multisig/test/global_hooks.ts
@@ -0,0 +1,19 @@
+import { env, EnvVars } from '@0x/dev-utils';
+
+import { coverage, profiler, provider } from '@0x/contracts-test-utils';
+
+before('start web3 provider engine', () => {
+ provider.start();
+});
+
+after('generate coverage report', async () => {
+ if (env.parseBoolean(EnvVars.SolidityCoverage)) {
+ const coverageSubprovider = coverage.getCoverageSubproviderSingleton();
+ await coverageSubprovider.writeCoverageAsync();
+ }
+ if (env.parseBoolean(EnvVars.SolidityProfiler)) {
+ const profilerSubprovider = profiler.getProfilerSubproviderSingleton();
+ await profilerSubprovider.writeProfilerOutputAsync();
+ }
+ provider.stop();
+});
diff --git a/contracts/multisig/test/multi_sig_with_time_lock.ts b/contracts/multisig/test/multi_sig_with_time_lock.ts
new file mode 100644
index 000000000..31c215505
--- /dev/null
+++ b/contracts/multisig/test/multi_sig_with_time_lock.ts
@@ -0,0 +1,349 @@
+import {
+ chaiSetup,
+ constants,
+ expectTransactionFailedAsync,
+ expectTransactionFailedWithoutReasonAsync,
+ increaseTimeAndMineBlockAsync,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { RevertReason } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import { LogWithDecodedArgs } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import {
+ MultiSigWalletWithTimeLockConfirmationEventArgs,
+ MultiSigWalletWithTimeLockConfirmationTimeSetEventArgs,
+ MultiSigWalletWithTimeLockContract,
+ MultiSigWalletWithTimeLockExecutionEventArgs,
+ MultiSigWalletWithTimeLockExecutionFailureEventArgs,
+ MultiSigWalletWithTimeLockSubmissionEventArgs,
+} from '../generated-wrappers/multi_sig_wallet_with_time_lock';
+import { TestRejectEtherContract } from '../generated-wrappers/test_reject_ether';
+import { artifacts } from '../src/artifacts';
+
+import { MultiSigWrapper } from './utils/multi_sig_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+// tslint:disable:no-unnecessary-type-assertion
+describe('MultiSigWalletWithTimeLock', () => {
+ let owners: string[];
+ let notOwner: string;
+ const REQUIRED_APPROVALS = new BigNumber(2);
+ const SECONDS_TIME_LOCKED = new BigNumber(1000000);
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ owners = [accounts[0], accounts[1], accounts[2]];
+ notOwner = accounts[3];
+ });
+
+ let multiSig: MultiSigWalletWithTimeLockContract;
+ let multiSigWrapper: MultiSigWrapper;
+
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+
+ describe('external_call', () => {
+ it('should be internal', async () => {
+ const secondsTimeLocked = new BigNumber(0);
+ multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync(
+ artifacts.MultiSigWalletWithTimeLock,
+ provider,
+ txDefaults,
+ owners,
+ REQUIRED_APPROVALS,
+ secondsTimeLocked,
+ );
+ expect(_.isUndefined((multiSig as any).external_call)).to.be.equal(true);
+ });
+ });
+ describe('confirmTransaction', () => {
+ let txId: BigNumber;
+ beforeEach(async () => {
+ const secondsTimeLocked = new BigNumber(0);
+ multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync(
+ artifacts.MultiSigWalletWithTimeLock,
+ provider,
+ txDefaults,
+ owners,
+ REQUIRED_APPROVALS,
+ secondsTimeLocked,
+ );
+ multiSigWrapper = new MultiSigWrapper(multiSig, provider);
+ const destination = notOwner;
+ const data = constants.NULL_BYTES;
+ const txReceipt = await multiSigWrapper.submitTransactionAsync(destination, data, owners[0]);
+ txId = (txReceipt.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>).args
+ .transactionId;
+ });
+ it('should revert if called by a non-owner', async () => {
+ await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.confirmTransactionAsync(txId, notOwner));
+ });
+ it('should revert if transaction does not exist', async () => {
+ const nonexistentTxId = new BigNumber(123456789);
+ await expectTransactionFailedWithoutReasonAsync(
+ multiSigWrapper.confirmTransactionAsync(nonexistentTxId, owners[1]),
+ );
+ });
+ it('should revert if transaction is already confirmed by caller', async () => {
+ await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.confirmTransactionAsync(txId, owners[0]));
+ });
+ it('should confirm transaction for caller and log a Confirmation event', async () => {
+ const txReceipt = await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
+ const log = txReceipt.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockConfirmationEventArgs>;
+ expect(log.event).to.be.equal('Confirmation');
+ expect(log.args.sender).to.be.equal(owners[1]);
+ expect(log.args.transactionId).to.be.bignumber.equal(txId);
+ });
+ it('should revert if fully confirmed', async () => {
+ await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
+ await expectTransactionFailedAsync(
+ multiSigWrapper.confirmTransactionAsync(txId, owners[2]),
+ RevertReason.TxFullyConfirmed,
+ );
+ });
+ it('should set the confirmation time of the transaction if it becomes fully confirmed', async () => {
+ const txReceipt = await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
+ const blockNum = await web3Wrapper.getBlockNumberAsync();
+ const timestamp = new BigNumber(await web3Wrapper.getBlockTimestampAsync(blockNum));
+ const log = txReceipt.logs[1] as LogWithDecodedArgs<MultiSigWalletWithTimeLockConfirmationTimeSetEventArgs>;
+ expect(log.args.confirmationTime).to.be.bignumber.equal(timestamp);
+ expect(log.args.transactionId).to.be.bignumber.equal(txId);
+ });
+ });
+ describe('executeTransaction', () => {
+ let txId: BigNumber;
+ const secondsTimeLocked = new BigNumber(1000000);
+ beforeEach(async () => {
+ multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync(
+ artifacts.MultiSigWalletWithTimeLock,
+ provider,
+ txDefaults,
+ owners,
+ REQUIRED_APPROVALS,
+ secondsTimeLocked,
+ );
+ multiSigWrapper = new MultiSigWrapper(multiSig, provider);
+ const destination = notOwner;
+ const data = constants.NULL_BYTES;
+ const txReceipt = await multiSigWrapper.submitTransactionAsync(destination, data, owners[0]);
+ txId = (txReceipt.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>).args
+ .transactionId;
+ });
+ it('should revert if transaction has not been fully confirmed', async () => {
+ await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber());
+ await expectTransactionFailedAsync(
+ multiSigWrapper.executeTransactionAsync(txId, owners[1]),
+ RevertReason.TxNotFullyConfirmed,
+ );
+ });
+ it('should revert if time lock has not passed', async () => {
+ await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
+ await expectTransactionFailedAsync(
+ multiSigWrapper.executeTransactionAsync(txId, owners[1]),
+ RevertReason.TimeLockIncomplete,
+ );
+ });
+ it('should execute a transaction and log an Execution event if successful and called by owner', async () => {
+ await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
+ await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber());
+ const txReceipt = await multiSigWrapper.executeTransactionAsync(txId, owners[1]);
+ const log = txReceipt.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockExecutionEventArgs>;
+ expect(log.event).to.be.equal('Execution');
+ expect(log.args.transactionId).to.be.bignumber.equal(txId);
+ });
+ it('should execute a transaction and log an Execution event if successful and called by non-owner', async () => {
+ await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
+ await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber());
+ const txReceipt = await multiSigWrapper.executeTransactionAsync(txId, notOwner);
+ const log = txReceipt.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockExecutionEventArgs>;
+ expect(log.event).to.be.equal('Execution');
+ expect(log.args.transactionId).to.be.bignumber.equal(txId);
+ });
+ it('should revert if a required confirmation is revoked before executeTransaction is called', async () => {
+ await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
+ await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber());
+ await multiSigWrapper.revokeConfirmationAsync(txId, owners[0]);
+ await expectTransactionFailedAsync(
+ multiSigWrapper.executeTransactionAsync(txId, owners[1]),
+ RevertReason.TxNotFullyConfirmed,
+ );
+ });
+ it('should revert if transaction has been executed', async () => {
+ await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
+ await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber());
+ const txReceipt = await multiSigWrapper.executeTransactionAsync(txId, owners[1]);
+ const log = txReceipt.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockExecutionEventArgs>;
+ expect(log.args.transactionId).to.be.bignumber.equal(txId);
+ await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.executeTransactionAsync(txId, owners[1]));
+ });
+ it("should log an ExecutionFailure event and not update the transaction's execution state if unsuccessful", async () => {
+ const contractWithoutFallback = await TestRejectEtherContract.deployFrom0xArtifactAsync(
+ artifacts.TestRejectEther,
+ provider,
+ txDefaults,
+ );
+ const data = constants.NULL_BYTES;
+ const value = new BigNumber(10);
+ const submissionTxReceipt = await multiSigWrapper.submitTransactionAsync(
+ contractWithoutFallback.address,
+ data,
+ owners[0],
+ { value },
+ );
+ const newTxId = (submissionTxReceipt.logs[0] as LogWithDecodedArgs<
+ MultiSigWalletWithTimeLockSubmissionEventArgs
+ >).args.transactionId;
+ await multiSigWrapper.confirmTransactionAsync(newTxId, owners[1]);
+ await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber());
+ const txReceipt = await multiSigWrapper.executeTransactionAsync(newTxId, owners[1]);
+ const executionFailureLog = txReceipt.logs[0] as LogWithDecodedArgs<
+ MultiSigWalletWithTimeLockExecutionFailureEventArgs
+ >;
+ expect(executionFailureLog.event).to.be.equal('ExecutionFailure');
+ expect(executionFailureLog.args.transactionId).to.be.bignumber.equal(newTxId);
+ });
+ });
+ describe('changeTimeLock', () => {
+ describe('initially non-time-locked', async () => {
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before('deploy a wallet', async () => {
+ const secondsTimeLocked = new BigNumber(0);
+ multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync(
+ artifacts.MultiSigWalletWithTimeLock,
+ provider,
+ txDefaults,
+ owners,
+ REQUIRED_APPROVALS,
+ secondsTimeLocked,
+ );
+ multiSigWrapper = new MultiSigWrapper(multiSig, provider);
+ });
+
+ it('should throw when not called by wallet', async () => {
+ return expectTransactionFailedWithoutReasonAsync(
+ multiSig.changeTimeLock.sendTransactionAsync(SECONDS_TIME_LOCKED, { from: owners[0] }),
+ );
+ });
+
+ it('should throw without enough confirmations', async () => {
+ const destination = multiSig.address;
+ const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(SECONDS_TIME_LOCKED);
+ const res = await multiSigWrapper.submitTransactionAsync(destination, changeTimeLockData, owners[0]);
+ const log = res.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+ return expectTransactionFailedAsync(
+ multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }),
+ RevertReason.TxNotFullyConfirmed,
+ );
+ });
+
+ it('should set confirmation time with enough confirmations', async () => {
+ const destination = multiSig.address;
+ const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(SECONDS_TIME_LOCKED);
+ const subRes = await multiSigWrapper.submitTransactionAsync(destination, changeTimeLockData, owners[0]);
+ const subLog = subRes.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>;
+ const txId = subLog.args.transactionId;
+
+ const confirmRes = await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
+ expect(confirmRes.logs).to.have.length(2);
+
+ const blockNum = await web3Wrapper.getBlockNumberAsync();
+ const blockInfo = await web3Wrapper.getBlockIfExistsAsync(blockNum);
+ if (_.isUndefined(blockInfo)) {
+ throw new Error(`Unexpectedly failed to fetch block at #${blockNum}`);
+ }
+ const timestamp = new BigNumber(blockInfo.timestamp);
+ const confirmationTimeBigNum = new BigNumber(await multiSig.confirmationTimes.callAsync(txId));
+
+ expect(timestamp).to.be.bignumber.equal(confirmationTimeBigNum);
+ });
+
+ it('should be executable with enough confirmations and secondsTimeLocked of 0', async () => {
+ const destination = multiSig.address;
+ const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(SECONDS_TIME_LOCKED);
+ const subRes = await multiSigWrapper.submitTransactionAsync(destination, changeTimeLockData, owners[0]);
+ const subLog = subRes.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>;
+ const txId = subLog.args.transactionId;
+
+ await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
+ await multiSigWrapper.executeTransactionAsync(txId, owners[1]);
+
+ const secondsTimeLocked = new BigNumber(await multiSig.secondsTimeLocked.callAsync());
+ expect(secondsTimeLocked).to.be.bignumber.equal(SECONDS_TIME_LOCKED);
+ });
+ });
+ describe('initially time-locked', async () => {
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ let txId: BigNumber;
+ const newSecondsTimeLocked = new BigNumber(0);
+ before('deploy a wallet, submit transaction to change timelock, and confirm the transaction', async () => {
+ multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync(
+ artifacts.MultiSigWalletWithTimeLock,
+ provider,
+ txDefaults,
+ owners,
+ REQUIRED_APPROVALS,
+ SECONDS_TIME_LOCKED,
+ );
+ multiSigWrapper = new MultiSigWrapper(multiSig, provider);
+
+ const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(newSecondsTimeLocked);
+ const res = await multiSigWrapper.submitTransactionAsync(
+ multiSig.address,
+ changeTimeLockData,
+ owners[0],
+ );
+ const log = res.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>;
+ txId = log.args.transactionId;
+ await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
+ });
+
+ it('should throw if it has enough confirmations but is not past the time lock', async () => {
+ return expectTransactionFailedAsync(
+ multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }),
+ RevertReason.TimeLockIncomplete,
+ );
+ });
+
+ it('should execute if it has enough confirmations and is past the time lock', async () => {
+ await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber());
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ const secondsTimeLocked = new BigNumber(await multiSig.secondsTimeLocked.callAsync());
+ expect(secondsTimeLocked).to.be.bignumber.equal(newSecondsTimeLocked);
+ });
+ });
+ });
+});
+// tslint:enable:no-unnecessary-type-assertion
diff --git a/contracts/multisig/test/utils/multi_sig_wrapper.ts b/contracts/multisig/test/utils/multi_sig_wrapper.ts
new file mode 100644
index 000000000..086143613
--- /dev/null
+++ b/contracts/multisig/test/utils/multi_sig_wrapper.ts
@@ -0,0 +1,54 @@
+import { LogDecoder } from '@0x/contracts-test-utils';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { MultiSigWalletContract } from '../../generated-wrappers/multi_sig_wallet';
+import { artifacts } from '../../src/artifacts';
+
+export class MultiSigWrapper {
+ private readonly _multiSig: MultiSigWalletContract;
+ private readonly _web3Wrapper: Web3Wrapper;
+ private readonly _logDecoder: LogDecoder;
+ constructor(multiSigContract: MultiSigWalletContract, provider: Provider) {
+ this._multiSig = multiSigContract;
+ this._web3Wrapper = new Web3Wrapper(provider);
+ this._logDecoder = new LogDecoder(this._web3Wrapper, artifacts);
+ }
+ public async submitTransactionAsync(
+ destination: string,
+ data: string,
+ from: string,
+ opts: { value?: BigNumber } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const value = _.isUndefined(opts.value) ? new BigNumber(0) : opts.value;
+ const txHash = await this._multiSig.submitTransaction.sendTransactionAsync(destination, value, data, {
+ from,
+ });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async confirmTransactionAsync(txId: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._multiSig.confirmTransaction.sendTransactionAsync(txId, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async revokeConfirmationAsync(txId: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._multiSig.revokeConfirmation.sendTransactionAsync(txId, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async executeTransactionAsync(
+ txId: BigNumber,
+ from: string,
+ opts: { gas?: number } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._multiSig.executeTransaction.sendTransactionAsync(txId, {
+ from,
+ gas: opts.gas,
+ });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+}
diff --git a/contracts/multisig/tsconfig.json b/contracts/multisig/tsconfig.json
new file mode 100644
index 000000000..6f381620e
--- /dev/null
+++ b/contracts/multisig/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "../../tsconfig",
+ "compilerOptions": {
+ "outDir": "lib",
+ "rootDir": ".",
+ "resolveJsonModule": true
+ },
+ "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
+ "files": [
+ "./generated-artifacts/MultiSigWallet.json",
+ "./generated-artifacts/MultiSigWalletWithTimeLock.json",
+ "./generated-artifacts/TestRejectEther.json"
+ ],
+ "exclude": ["./deploy/solc/solc_bin"]
+}
diff --git a/contracts/multisig/tslint.json b/contracts/multisig/tslint.json
new file mode 100644
index 000000000..1bb3ac2a2
--- /dev/null
+++ b/contracts/multisig/tslint.json
@@ -0,0 +1,6 @@
+{
+ "extends": ["@0x/tslint-config"],
+ "rules": {
+ "custom-no-magic-numbers": false
+ }
+}