Repository: christianlundkvist/simple-multisig Branch: master Commit: 560c463c8651 Files: 13 Total size: 29.4 KB Directory structure: gitextract_xlawnlp0/ ├── .gitignore ├── LICENSE.txt ├── README.md ├── RELEASE-NOTES.md ├── browsertest/ │ ├── index.html │ └── sign.js ├── contracts/ │ ├── SimpleMultiSig.sol │ └── TestRegistry.sol ├── maurelian_review.md ├── migrations/ │ └── placeholder.txt ├── package.json ├── test/ │ └── simplemultisig.js └── truffle.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules build *.orig ================================================ FILE: LICENSE.txt ================================================ Copyright (c) 2017 Christian Lundkvist Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # simple-multisig ## Introduction This is an Ethereum multisig contract designed to be as simple as possible. It is described further in this [medium post](https://medium.com/@ChrisLundkvist/exploring-simpler-ethereum-multisig-contracts-b71020c19037). The main idea behind the contract is to pass in a threshold of detached signatures into the `execute` function and the contract will check the signatures and send off the transaction. The audit report by [ConsenSys Diligence'](https://consensys.net/diligence/) can be found [here](./audit.pdf). ## Version 2.0.0 Update to EIP712 In version 2.0.0 the Simple Multisig was updated to use the EIP712 signature standard. This means that the signature format of the previous version is no longer compatible. If your contract is already deployed and in use it still works but that version will no longer be supported in the future. We recommend moving ETH and tokens over to a newly deployed contract and using the EIP712 format going forward. Another change to be aware of is that the constructor now takes an extra parameter `chainId` to specify which network the contract is deployed on. ## Data to be signed The Simple MultiSig uses the [EIP712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md) standard to package and hash the data to be signed. Each signer will sign a message over the following data fields, which encode the ethereum transaction to execute: * `address destination` - The target address for the transaction * `uint256 value` - The value of the transaction expressed in Wei * `bytes data` - The data of the transaction in hex format * `uint256 nonce` - The nonce for this transaction. Must match the current nonce in the multisig contract. * `address executor` - Specifies which Ethereum address is allowed to call the `execute` function. It is allowed to specify the zero address as an executor, in which case any address can call the `execute` function. This field is mainly to address concerns about replay attacks in some edge cases. * `uint256 gasLimit` - Specifies how much gas to pass on to the final `call`, independently of how much gas is supplied to the transaction calling the `execute` function. This can be used to constrain what kind of computations can be performed by the target smart contract. If the signers do not need this level of control a very high gasLimit of several million can be used for this data field. The data to be signed also includes the following EIP712 Domain data that specifies the context of the signed data: * Name (`"Simple MultiSig"`) * Version (`"1"`) * ChainId (Integer marking current chain, e.g. 1 for mainnet) * Contract Address (Address of the specific multisig contract instance) * Salt (`0x251543af6a222378665a76fe38dbceae4871a070b7fdaf5c6c30cf758dc33cc0`, unique identifier specific to SimpleMultisig) ## Installation and testing Install global dependencies: * `npm install -g truffle` * `npm install -g ganache-cli` To run the tests: * Make sure `ganache-cli` is running in its own terminal window. * `npm install` * `npm run test` ## Testing signatures in a browser If you have the [MetaMask](https://metamask.io) browser extension you can open the page `browsertest/index.html` in your browser and test signing data. The signature will be returned in a `(r,s,v)` format which can be plugged into the `execute` function. ================================================ FILE: RELEASE-NOTES.md ================================================ # Release Notes # ## Version 2.0.1 - 2019-06-19 * Fix misspellings in contract comments. By [ethers](https://github.com/ethers). * Update browser test with check for web3 object. * Fix faulty documentation of private key in MetaMask browser test. ## Version 2.0.0 - 2018-08-18 ## * Backwards incompatible update of main contract to support [EIP712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md). * Add `executor` to the signed data in order to specify which address needs to call the `execute` function. Allows `address(0)` as valid executor if the signers want anyone to be able to execute the transaction. * Add `gasLimit` to the signed data in order to specify how much gas to supply to the function call. * Add input parameter `chainId` to the constructor. * Change fallback function from `public` to `external`. * Update tests for EIP712. * Add test for wrong nonce. * Update Solidity compiler version to 0.4.24. * Remove use of `bignumber.js` and replace with `web3.toBigNumber()` (Thanks to [barakman](https://github.com/barakman)). ## Version 1.0.4 - 2018-06-12 ## * Document owners_ address being strictly increasing, by [ripper234](https://github.com/ripper234) * Update to new constructor syntax, by [ripper234](https://github.com/ripper234) * Check that threshold is positive instead of non-zero, by [ripper234](https://github.com/ripper234) * Update .gitignore, by [ripper234](https://github.com/ripper234) ## Version 1.0.3 - 2018-06-11 ## * Moved the assembly to inside the `execute()` function and removed the `executeCall()` function. This is to avoid the possibility of the `internal` keyword on the `executeCall()` function being accidentally removed which would have catastrophic consequences. ## Version 1.0.2 - 2018-05-04 ## * Updated to use assembly instead of `address.call()` syntax. Thanks to [ethers](https://github.com/ethers) for the suggestion. For more info about the problems with `address.call()` see [here](https://github.com/ethereum/solidity/issues/2884). * Fix indentation mismatch. ## Version 1.0.1 - 2018-05-04 ## * Update to work with latest Solidity and Truffle version. By [grempe](https://github.com/grempe) * Add RELEASE-NOTES ## Version 1.0.0 - 2017-03 to 2017-11 ## * Initial implementation * Tweaks by [naterush](https://github.com/naterush) * Informal review and fixes by [maurelian](https://github.com/maurelian) * Replace `sha3` with `keccak256` by [ethers](https://github.com/ethers) * Add MIT license ================================================ FILE: browsertest/index.html ================================================ EIP712 browser demo

This page tests signing a SimpleMultiSig transaction using EIP712. It is based on this EIP712 demo by Wei Jie Koh.

Wallet address
Destination
Value
Data
Nonce
Executor
GasLimit

Signed Data

================================================ FILE: browsertest/sign.js ================================================ function parseSignature(signature) { var r = signature.substring(0, 64); var s = signature.substring(64, 128); var v = signature.substring(128, 130); return { r: "0x" + r, s: "0x" + s, v: parseInt(v, 16) } } window.onload = function (e) { // force the user to unlock their MetaMask if (web3.eth.accounts[0] == null) { alert("Please unlock MetaMask first"); web3.currentProvider.enable().catch(alert); } var signBtn = document.getElementById("signBtn"); signBtn.onclick = function(e) { if (web3.eth.accounts[0] == null) { return; } const domain = [ { name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" }, { name: "salt", type: "bytes32" } ]; const multiSigTx = [ { name: "destination", type: "address" }, { name: "value", type: "uint256" }, { name: "data", type: "bytes" }, { name: "nonce", type: "uint256" }, { name: "executor", type: "address" }, { name: "gasLimit", type: "uint256" } ]; const domainData = { name: "Simple MultiSig", version: "1", chainId: parseInt(web3.version.network, 10), verifyingContract: document.getElementById("walletAddress").value, salt: "0x251543af6a222378665a76fe38dbceae4871a070b7fdaf5c6c30cf758dc33cc0" }; var message = { destination: document.getElementById("destination").value, value: document.getElementById("value").value, data: document.getElementById("data").value, nonce: parseInt(document.getElementById("nonce").value, 10), executor: document.getElementById("executor").value, gasLimit: parseInt(document.getElementById("gasLimit").value, 10), }; const data = JSON.stringify({ types: { EIP712Domain: domain, MultiSigTransaction: multiSigTx }, domain: domainData, primaryType: "MultiSigTransaction", message: message }); console.log(data) const signer = web3.eth.accounts[0]; console.log(signer) web3.currentProvider.sendAsync( { method: "eth_signTypedData_v3", params: [signer, data], from: signer }, function(err, result) { if (err || result.error) { return console.error(result); } const signature = parseSignature(result.result.substring(2)); document.getElementById("signedData").value = "r: " + signature.r + "\ns: " + signature.s + "\nv: " + signature.v } ); }; } ================================================ FILE: contracts/SimpleMultiSig.sol ================================================ pragma solidity ^0.4.24; contract SimpleMultiSig { // EIP712 Precomputed hashes: // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)") bytes32 constant EIP712DOMAINTYPE_HASH = 0xd87cd6ef79d4e2b95e15ce8abf732db51ec771f1ca2edccf22a46c729ac56472; // keccak256("Simple MultiSig") bytes32 constant NAME_HASH = 0xb7a0bfa1b79f2443f4d73ebb9259cddbcd510b18be6fc4da7d1aa7b1786e73e6; // keccak256("1") bytes32 constant VERSION_HASH = 0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6; // keccak256("MultiSigTransaction(address destination,uint256 value,bytes data,uint256 nonce,address executor,uint256 gasLimit)") bytes32 constant TXTYPE_HASH = 0x3ee892349ae4bbe61dce18f95115b5dc02daf49204cc602458cd4c1f540d56d7; bytes32 constant SALT = 0x251543af6a222378665a76fe38dbceae4871a070b7fdaf5c6c30cf758dc33cc0; uint public nonce; // (only) mutable state uint public threshold; // immutable state mapping (address => bool) isOwner; // immutable state address[] public ownersArr; // immutable state bytes32 DOMAIN_SEPARATOR; // hash for EIP712, computed from contract address // Note that owners_ must be strictly increasing, in order to prevent duplicates constructor(uint threshold_, address[] owners_, uint chainId) public { require(owners_.length <= 10 && threshold_ <= owners_.length && threshold_ > 0); address lastAdd = address(0); for (uint i = 0; i < owners_.length; i++) { require(owners_[i] > lastAdd); isOwner[owners_[i]] = true; lastAdd = owners_[i]; } ownersArr = owners_; threshold = threshold_; DOMAIN_SEPARATOR = keccak256(abi.encode(EIP712DOMAINTYPE_HASH, NAME_HASH, VERSION_HASH, chainId, this, SALT)); } // Note that address recovered from signatures must be strictly increasing, in order to prevent duplicates function execute(uint8[] sigV, bytes32[] sigR, bytes32[] sigS, address destination, uint value, bytes data, address executor, uint gasLimit) public { require(sigR.length == threshold); require(sigR.length == sigS.length && sigR.length == sigV.length); require(executor == msg.sender || executor == address(0)); // EIP712 scheme: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md bytes32 txInputHash = keccak256(abi.encode(TXTYPE_HASH, destination, value, keccak256(data), nonce, executor, gasLimit)); bytes32 totalHash = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, txInputHash)); address lastAdd = address(0); // cannot have address(0) as an owner for (uint i = 0; i < threshold; i++) { address recovered = ecrecover(totalHash, sigV[i], sigR[i], sigS[i]); require(recovered > lastAdd && isOwner[recovered]); lastAdd = recovered; } // If we make it here all signatures are accounted for. // The address.call() syntax is no longer recommended, see: // https://github.com/ethereum/solidity/issues/2884 nonce = nonce + 1; bool success = false; assembly { success := call(gasLimit, destination, value, add(data, 0x20), mload(data), 0, 0) } require(success); } function () payable external {} } ================================================ FILE: contracts/TestRegistry.sol ================================================ pragma solidity ^0.4.18; // This contract is only used for testing purposes. contract TestRegistry { mapping(address => uint) public registry; function register(uint x) payable public { registry[msg.sender] = x; } } ================================================ FILE: maurelian_review.md ================================================ # Smart contract review by maurelian ## Introduction by christianlundkvist This is an informal review of the `SimpleMultisig` smart contract by [maurelian](https://github.com/maurelian). All the issues raised were also fixed by maurelian in subsequent PRs. The original review is also available [here](https://gist.github.com/maurelian/f6b842854edec7d02a1f46be1f6e2a67). Some minor stylistic alterations were made. ## Introduction This is an informal... you might even say adhoc review of the `SimpleMultisig` contract, found here: This review makes no legally binding guarantees whatsoever. Use at your own risk. ## Summary The two findings listed under `Major` and `Medium` should be fixed. The `Minor` and `Note` issues don't pose a security risk, but should be fixed to adhere to best practices. Otherwise, no significant issues were identified. This contract appears to work as advertised, and its simplicity is excellent for the task. ## Specific findings ### Major: contract can be "bricked" on deployment if same address is used twice The contract could be deployed, and instantly unusable. This would occur if the same address is added twice, and the threshold requires all owners to sign in order to execute. The constructor should be modified to prevent this using a similar approach to the `execute` function. ### Medium: Upgrade to solidity ^0.4.14 Prior to 0.4.14, a bug existed in `ecrecover`. Ref: ### Minor: use `require` instead of `throw` It's easier to read, and `throw` is being deprecated. ### Minor: Indentation of test suite The utility functions were not properly indented from line 118 to 148 of `multisig.js`. ### Note: Clarify the value of the requirement that signatures be submitted in ascending order I belive the purpose is to facilitate checking against duplicate signatures, but it would be nice to make that explicit. ================================================ FILE: migrations/placeholder.txt ================================================ So git adds folder, so truffle runs tests :~) ================================================ FILE: package.json ================================================ { "name": "simple-multisig", "version": "1.0.4", "description": "Simple Ethereum multisig contract", "main": "test/simplemultisig.js", "directories": { "test": "test" }, "scripts": { "test": "truffle test" }, "repository": { "type": "git", "url": "git+https://github.com/christianlundkvist/simple-multisig.git" }, "keywords": [ "Ethereum", "Wallet" ], "author": "christian.lundkvist@gmail.com", "license": "MIT", "bugs": { "url": "https://github.com/christianlundkvist/simple-multisig/issues" }, "homepage": "https://github.com/christianlundkvist/simple-multisig#readme", "devDependencies": { "eth-lightwallet": "*", "bluebird": "*" } } ================================================ FILE: test/simplemultisig.js ================================================ var SimpleMultiSig = artifacts.require("./SimpleMultiSig.sol") var TestRegistry = artifacts.require("./TestRegistry.sol") var lightwallet = require('eth-lightwallet') const Promise = require('bluebird') const web3SendTransaction = Promise.promisify(web3.eth.sendTransaction) const web3GetBalance = Promise.promisify(web3.eth.getBalance) let DOMAIN_SEPARATOR const TXTYPE_HASH = '0x3ee892349ae4bbe61dce18f95115b5dc02daf49204cc602458cd4c1f540d56d7' const NAME_HASH = '0xb7a0bfa1b79f2443f4d73ebb9259cddbcd510b18be6fc4da7d1aa7b1786e73e6' const VERSION_HASH = '0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6' const EIP712DOMAINTYPE_HASH = '0xd87cd6ef79d4e2b95e15ce8abf732db51ec771f1ca2edccf22a46c729ac56472' const SALT = '0x251543af6a222378665a76fe38dbceae4871a070b7fdaf5c6c30cf758dc33cc0' const CHAINID = 1 const ZEROADDR = '0x000000000000000000000000000000000000000000000' contract('SimpleMultiSig', function(accounts) { let keyFromPw let acct let lw let createSigs = function(signers, multisigAddr, nonce, destinationAddr, value, data, executor, gasLimit) { const domainData = EIP712DOMAINTYPE_HASH + NAME_HASH.slice(2) + VERSION_HASH.slice(2) + CHAINID.toString('16').padStart(64, '0') + multisigAddr.slice(2).padStart(64, '0') + SALT.slice(2) DOMAIN_SEPARATOR = web3.sha3(domainData, {encoding: 'hex'}) let txInput = TXTYPE_HASH + destinationAddr.slice(2).padStart(64, '0') + value.toString('16').padStart(64, '0') + web3.sha3(data, {encoding: 'hex'}).slice(2) + nonce.toString('16').padStart(64, '0') + executor.slice(2).padStart(64, '0') + gasLimit.toString('16').padStart(64, '0') let txInputHash = web3.sha3(txInput, {encoding: 'hex'}) let input = '0x19' + '01' + DOMAIN_SEPARATOR.slice(2) + txInputHash.slice(2) let hash = web3.sha3(input, {encoding: 'hex'}) let sigV = [] let sigR = [] let sigS = [] for (var i=0; i { let seed = "pull rent tower word science patrol economy legal yellow kit frequent fat" lightwallet.keystore.createVault( {hdPathString: "m/44'/60'/0'/0", seedPhrase: seed, password: "test", salt: "testsalt" }, function (err, keystore) { lw = keystore lw.keyFromPassword("test", function(e,k) { keyFromPw = k lw.generateNewAddress(keyFromPw, 20) let acctWithout0x = lw.getAddresses() acct = acctWithout0x.map((a) => {return a}) acct.sort() done() }) }) }) describe("3 signers, threshold 2", () => { it("should succeed with signers 0, 1", (done) => { let signers = [acct[0], acct[1]] signers.sort() executeSendSuccess(acct.slice(0,3), 2, signers, done) }) it("should succeed with signers 0, 2", (done) => { let signers = [acct[0], acct[2]] signers.sort() executeSendSuccess(acct.slice(0,3), 2, signers, done) }) it("should succeed with signers 1, 2", (done) => { let signers = [acct[1], acct[2]] signers.sort() executeSendSuccess(acct.slice(0,3), 2, signers, done) }) it("should fail due to non-owner signer", (done) => { let signers = [acct[0], acct[3]] signers.sort() executeSendFailure(acct.slice(0,3), 2, signers, 0, accounts[0], 100000, done) }) it("should fail with more signers than threshold", (done) => { executeSendFailure(acct.slice(0,3), 2, acct.slice(0,3), 0, accounts[0], 100000, done) }) it("should fail with fewer signers than threshold", (done) => { executeSendFailure(acct.slice(0,3), 2, [acct[0]], 0, accounts[0], 100000, done) }) it("should fail with one signer signing twice", (done) => { executeSendFailure(acct.slice(0,3), 2, [acct[0], acct[0]], 0, accounts[0], 100000, done) }) it("should fail with signers in wrong order", (done) => { let signers = [acct[0], acct[1]] signers.sort().reverse() //opposite order it should be executeSendFailure(acct.slice(0,3), 2, signers, 0, accounts[0], 100000, done) }) it("should fail with the wrong nonce", (done) => { const nonceOffset = 1 executeSendFailure(acct.slice(0,3), 2, [acct[0], acct[1]], nonceOffset, accounts[0], 100000, done) }) }) describe("Edge cases", () => { it("should succeed with 10 owners, 10 signers", (done) => { executeSendSuccess(acct.slice(0,10), 10, acct.slice(0,10), done) }) it("should fail to create with signers 0, 0, 2, and threshold 3", (done) => { creationFailure([acct[0],acct[0],acct[2]], 3, done) }) it("should fail with 0 signers", (done) => { executeSendFailure(acct.slice(0,3), 2, [], 0, accounts[0], 100000, done) }) it("should fail with 11 owners", (done) => { creationFailure(acct.slice(0,11), 2, done) }) }) describe("Hash constants", () => { it("uses correct hash for EIP712DOMAINTYPE", (done) => { const eip712DomainType = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)' assert.equal(web3.sha3(eip712DomainType), EIP712DOMAINTYPE_HASH) done() }) it("uses correct hash for NAME", (done) => { assert.equal(web3.sha3('Simple MultiSig'), NAME_HASH) done() }) it("uses correct hash for VERSION", (done) => { assert.equal(web3.sha3('1'), VERSION_HASH) done() }) it("uses correct hash for MULTISIGTX", (done) => { const multiSigTxType = 'MultiSigTransaction(address destination,uint256 value,bytes data,uint256 nonce,address executor,uint256 gasLimit)' assert.equal(web3.sha3(multiSigTxType), TXTYPE_HASH) done() }) }) describe("Browser MetaMask test", () => { it("Matches the signature from MetaMask", (done) => { // To test in MetaMask: // // Import the following private key in MetaMask: // 0xac6d4b13220cd81f3630b7714f7e205494acc0823fb07a63bb40e65f669cbb9e // It should give the address: // 0x01BF9878a7099b2203838f3a8E7652Ad7B127A26 // // Make sure you are on Mainnet with the above account // Load the HTML page located at // browsertest/index.html // and click "Sign data" (using the default values). // You should see the signature values r,s,v below: const mmSigR = '0x91a622ccbd1c65debc16cfa1761b6200acc42099a19d753c7c59ceb12a8f5cfc' const mmSigS = '0x6814fae69a6cc506b11adf971ca233fbcdbdca312ab96a58eb6b6b6792771fd4' const mmSigV = 27 const walletAddress = '0xe3de7de481cbde9b4d5f62c6d228ec62277560c8' const destination = '0x8582afea2dd8e47297dbcdcf9ca289756ee21430' const value = web3.toWei(web3.toBigNumber(0.01), 'ether') const data = '0xf207564e0000000000000000000000000000000000000000000000000000000000003039' const nonce = 2 const executor = '0x0be430662ec0659ee786c04925c0146991fbdc0f' const gasLimit = 100000 const signers = [acct[0]] let sigs = createSigs(signers, walletAddress, nonce, destination, value, data, executor, gasLimit) assert.equal(sigs.sigR[0], mmSigR) assert.equal(sigs.sigS[0], mmSigS) assert.equal(sigs.sigV[0], mmSigV) done() }) }) }) ================================================ FILE: truffle.js ================================================ module.exports = { networks: { development: { host: "localhost", port: 8545, network_id: "*" // Match any network id } } };