master 560c463c8651 cached
13 files
29.4 KB
8.8k tokens
9 symbols
1 requests
Download .txt
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
================================================
<head>
  <meta charset="utf-8">
  <title>EIP712 browser demo</title>
</head>
<body>
  <script src="./sign.js" language="javascript"></script>
  <p>
    This page tests signing a SimpleMultiSig transaction using EIP712. It is based on <a href="https://weijiekoh.github.io/eip712-signing-demo/index.html">this EIP712 demo</a> by Wei Jie Koh.
  </p>

  <table>
    <thead>
    </thead>
    <tbody>
      <tr>
        <td>Wallet address</td>
        <td><input type="text" id="walletAddress" size="40" value="0xe3de7de481cbde9b4d5f62c6d228ec62277560c8"></td>
      </tr>
      <tr>
        <td>Destination</td>
        <td><input type="text" id="destination" size="40" value="0x8582afea2dd8e47297dbcdcf9ca289756ee21430"></td>
      </tr>
      <tr>
        <td>Value</td>
        <td><input type="text" id="value" size="10" value="10000000000000000"></td>
      </tr>
      <tr>
        <td>Data</td>
        <td><input type="text" id="data" size="40" value="0xf207564e0000000000000000000000000000000000000000000000000000000000003039"></td>
      </tr>
      <tr>
        <td>Nonce</td>
        <td><input type="text" id="nonce" size="10" value="2"/></td>
      </tr>
      <tr>
        <td>Executor</td>
        <td><input type="text" id="executor" size="40" value="0x0be430662ec0659ee786c04925c0146991fbdc0f"></td>
      </tr>
      <tr>
        <td>GasLimit</td>
        <td><input type="text" id="gasLimit" size="10" value="100000"></td>
      </tr>
    </tbody>
  </table>

  <button id="signBtn">Sign data</button>
  
  <h3>Signed Data</h3>
  <div><textarea id='signedData' rows=8 cols=40></textarea></div>
  

</body>
  


================================================
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: <https://github.com/christianlundkvist/simple-multisig/tree/9d486cb280c1b0108a64a0e1c4bc0c636919c2d7>

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: <https://github.com/ConsenSys/0x_review/blob/final/report/3_general_findings.md#ecrecover-issue-in-solidity-0414>


### 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<signers.length; i++) {
      let sig = lightwallet.signing.signMsgHash(lw, keyFromPw, hash, signers[i])
      sigV.push(sig.v)
      sigR.push('0x' + sig.r.toString('hex'))
      sigS.push('0x' + sig.s.toString('hex'))
    }

    // if (signers[0] == acct[0]) {
    //   console.log("Signer: " + signers[0])
    //   console.log("Wallet address: " + multisigAddr)
    //   console.log("Destination: " + destinationAddr)
    //   console.log("Value: " + value)
    //   console.log("Data: " + data)
    //   console.log("Nonce: " + nonce)
    //   console.log("Executor: " + executor)
    //   console.log("gasLimit: " + gasLimit)
    //   console.log("r: " + sigR[0])
    //   console.log("s: " + sigS[0])
    //   console.log("v: " + sigV[0])
    // }
      
    return {sigV: sigV, sigR: sigR, sigS: sigS}

  }

  let executeSendSuccess = async function(owners, threshold, signers, done) {

    let multisig = await SimpleMultiSig.new(threshold, owners, CHAINID, {from: accounts[0]})
    let randomAddr = web3.sha3(Math.random().toString()).slice(0,42)
    let executor = accounts[0]
    let msgSender = accounts[0]
    
    // Receive funds
    await web3SendTransaction({from: accounts[0], to: multisig.address, value: web3.toWei(web3.toBigNumber(0.1), 'ether')})

    let nonce = await multisig.nonce.call()
    assert.equal(nonce.toNumber(), 0)

    let bal = await web3GetBalance(multisig.address)
    assert.equal(bal, web3.toWei(0.1, 'ether'))

    // check that owners are stored correctly
    for (var i=0; i<owners.length; i++) {
      let ownerFromContract = await multisig.ownersArr.call(i)
      assert.equal(owners[i], ownerFromContract)
    }

    let value = web3.toWei(web3.toBigNumber(0.01), 'ether')

    let sigs = createSigs(signers, multisig.address, nonce, randomAddr, value, '', executor, 21000)

    await multisig.execute(sigs.sigV, sigs.sigR, sigs.sigS, randomAddr, value, '', executor, 21000, {from: msgSender, gasLimit: 1000000})

    // Check funds sent
    bal = await web3GetBalance(randomAddr)
    assert.equal(bal.toString(), value.toString())

    // Check nonce updated
    nonce = await multisig.nonce.call()
    assert.equal(nonce.toNumber(), 1)

    // Send again
    // Check that it succeeds with executor = Zero address
    sigs = createSigs(signers, multisig.address, nonce, randomAddr, value, '', ZEROADDR, 21000)
    await multisig.execute(sigs.sigV, sigs.sigR, sigs.sigS, randomAddr, value, '', ZEROADDR, 21000, {from: msgSender, gasLimit: 1000000})

    // Check funds
    bal = await web3GetBalance(randomAddr)
    assert.equal(bal.toString(), (value*2).toString())

    // Check nonce updated
    nonce = await multisig.nonce.call()
    assert.equal(nonce.toNumber(), 2)

    // Test contract interactions
    let reg = await TestRegistry.new({from: accounts[0]})

    let number = 12345
    let data = lightwallet.txutils._encodeFunctionTxData('register', ['uint256'], [number])

    sigs = createSigs(signers, multisig.address, nonce, reg.address, value, data, executor, 100000)
    await multisig.execute(sigs.sigV, sigs.sigR, sigs.sigS, reg.address, value, data, executor, 100000, {from: msgSender, gasLimit: 1000000})

    // Check that number has been set in registry
    let numFromRegistry = await reg.registry(multisig.address)
    assert.equal(numFromRegistry.toNumber(), number)

    // Check funds in registry
    bal = await web3GetBalance(reg.address)
    assert.equal(bal.toString(), value.toString())

    // Check nonce updated
    nonce = await multisig.nonce.call()
    assert.equal(nonce.toNumber(), 3)

    done()
  }

  let executeSendFailure = async function(owners, threshold, signers, nonceOffset, executor, gasLimit, done) {

    let multisig = await SimpleMultiSig.new(threshold, owners, CHAINID, {from: accounts[0]})

    let nonce = await multisig.nonce.call()
    assert.equal(nonce.toNumber(), 0)

    // Receive funds
    await web3SendTransaction({from: accounts[0], to: multisig.address, value: web3.toWei(web3.toBigNumber(2), 'ether')})

    let randomAddr = web3.sha3(Math.random().toString()).slice(0,42)
    let value = web3.toWei(web3.toBigNumber(0.1), 'ether')
    let sigs = createSigs(signers, multisig.address, nonce + nonceOffset, randomAddr, value, '', executor, gasLimit)

    let errMsg = ''
    try {
      await multisig.execute(sigs.sigV, sigs.sigR, sigs.sigS, randomAddr, value, '', executor, gasLimit, {from: executor, gasLimit: 1000000})
    }
    catch(error) {
      errMsg = error.message
    }

    assert.equal(errMsg, 'VM Exception while processing transaction: revert', 'Test did not throw')

    done()
  }

  let creationFailure = async function(owners, threshold, done) {

    try {
      await SimpleMultiSig.new(threshold, owners, CHAINID, {from: accounts[0]})
    }
    catch(error) {
      errMsg = error.message
    }

    assert.equal(errMsg, 'VM Exception while processing transaction: revert', 'Test did not throw')

    done()
  }
  
  before((done) => {

    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
    }
  }
};
Download .txt
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
Download .txt
SYMBOL INDEX (9 symbols across 2 files)

FILE: browsertest/sign.js
  function parseSignature (line 1) | function parseSignature(signature) {

FILE: test/simplemultisig.js
  constant DOMAIN_SEPARATOR (line 9) | let DOMAIN_SEPARATOR
  constant TXTYPE_HASH (line 10) | const TXTYPE_HASH = '0x3ee892349ae4bbe61dce18f95115b5dc02daf49204cc60245...
  constant NAME_HASH (line 11) | const NAME_HASH = '0xb7a0bfa1b79f2443f4d73ebb9259cddbcd510b18be6fc4da7d1...
  constant VERSION_HASH (line 12) | const VERSION_HASH = '0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cd...
  constant EIP712DOMAINTYPE_HASH (line 13) | const EIP712DOMAINTYPE_HASH = '0xd87cd6ef79d4e2b95e15ce8abf732db51ec771f...
  constant SALT (line 14) | const SALT = '0x251543af6a222378665a76fe38dbceae4871a070b7fdaf5c6c30cf75...
  constant CHAINID (line 17) | const CHAINID = 1
  constant ZEROADDR (line 18) | const ZEROADDR = '0x000000000000000000000000000000000000000000000'
Condensed preview — 13 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (32K chars).
[
  {
    "path": ".gitignore",
    "chars": 25,
    "preview": "node_modules\nbuild\n*.orig"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1063,
    "preview": "Copyright (c) 2017 Christian Lundkvist\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
  },
  {
    "path": "README.md",
    "chars": 3375,
    "preview": "# simple-multisig\n\n## Introduction\n\nThis is an Ethereum multisig contract designed to be as simple as possible. It is de"
  },
  {
    "path": "RELEASE-NOTES.md",
    "chars": 2501,
    "preview": "# Release Notes #\n\n## Version 2.0.1 - 2019-06-19\n\n* Fix misspellings in contract comments. By [ethers](https://github.co"
  },
  {
    "path": "browsertest/index.html",
    "chars": 1624,
    "preview": "<head>\n  <meta charset=\"utf-8\">\n  <title>EIP712 browser demo</title>\n</head>\n<body>\n  <script src=\"./sign.js\" language=\""
  },
  {
    "path": "browsertest/sign.js",
    "chars": 2626,
    "preview": "function parseSignature(signature) {\n  var r = signature.substring(0, 64);\n  var s = signature.substring(64, 128);\n  var"
  },
  {
    "path": "contracts/SimpleMultiSig.sol",
    "chars": 3435,
    "preview": "pragma solidity ^0.4.24;\n\ncontract SimpleMultiSig {\n\n// EIP712 Precomputed hashes:\n// keccak256(\"EIP712Domain(string nam"
  },
  {
    "path": "contracts/TestRegistry.sol",
    "chars": 230,
    "preview": "pragma solidity ^0.4.18;\n\n// This contract is only used for testing purposes.\ncontract TestRegistry {\n\n  mapping(address"
  },
  {
    "path": "maurelian_review.md",
    "chars": 2097,
    "preview": "# Smart contract review by maurelian\n\n## Introduction by christianlundkvist\n\nThis is an informal review of the `SimpleMu"
  },
  {
    "path": "migrations/placeholder.txt",
    "chars": 46,
    "preview": "So git adds folder, so truffle runs tests :~)\n"
  },
  {
    "path": "package.json",
    "chars": 711,
    "preview": "{\n  \"name\": \"simple-multisig\",\n  \"version\": \"1.0.4\",\n  \"description\": \"Simple Ethereum multisig contract\",\n  \"main\": \"te"
  },
  {
    "path": "test/simplemultisig.js",
    "chars": 12206,
    "preview": "var SimpleMultiSig = artifacts.require(\"./SimpleMultiSig.sol\")\nvar TestRegistry = artifacts.require(\"./TestRegistry.sol\""
  },
  {
    "path": "truffle.js",
    "chars": 154,
    "preview": "module.exports = {\n  networks: {\n    development: {\n      host: \"localhost\",\n      port: 8545,\n      network_id: \"*\" // "
  }
]

About this extraction

This page contains the full source code of the christianlundkvist/simple-multisig GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 13 files (29.4 KB), approximately 8.8k tokens, and a symbol index with 9 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!