0

This code seems to be a good standard for multisig accounts on the eth chain. Could someone explain it piece by piece for me?

https://github.com/christianlundkvist/simple-multisig

pragma solidity 0.4.15;
contract SimpleMultiSig {

  uint public nonce;                // (only) mutable state
  uint public threshold;            // immutable state
  mapping (address => bool) isOwner; // immutable state
  address[] public ownersArr;        // immutable state

  function SimpleMultiSig(uint threshold_, address[] owners_) {
    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_;
  }

  // Note that address recovered from signatures must be strictly increasing
  function execute(uint8[] sigV, bytes32[] sigR, bytes32[] sigS, address destination, uint value, bytes data) {
    require(sigR.length == threshold);
    require(sigR.length == sigS.length && sigR.length == sigV.length);

    // Follows ERC191 signature scheme: https://github.com/ethereum/EIPs/issues/191
    bytes32 txHash = keccak256(byte(0x19), byte(0), this, destination, value, data, nonce);

    address lastAdd = address(0); // cannot have address(0) as an owner
    for (uint i = 0; i < threshold; i++) {
        address recovered = ecrecover(txHash, sigV[i], sigR[i], sigS[i]);
        require(recovered > lastAdd && isOwner[recovered]);
        lastAdd = recovered;
    }

    // If we make it here all signatures are accounted for
    nonce = nonce + 1;
    require(destination.call.value(value)(data));
  }

  function () payable {}
}
0

I can give you an explanation to the best of my knowledge. And as detailed as possible without going into too much meaningless details.

The contract you provided seems like a simplified multisig contract which creates a wallet contract with up to 10 owners and an X amount of required confirmations in order to make transfers.

First Part (Variables)

pragma solidity 0.4.15;
contract SimpleMultiSig {

  uint public nonce;                // (only) mutable state
  uint public threshold;            // immutable state
  mapping (address => bool) isOwner; // immutable state
  address[] public ownersArr;        // immutable state

The first part is fairly trivial. The contract declares the following variables:

  • nonce - a simple counter which increments for each new transaction. Helps prevent replay attacks (simple explanation of nonce here)
  • threshold - the number of required confirmations to execute a transaction
  • isOwner - a mapping which is used to set and find if an address is an owner
  • ownersArr - an array listing the owner's addresses

Owner Datastructure

The isOwner and ownersArr are used together to create a more complex data structure. The map allows you to easily find a value by supplying the associated key, however in ethereum maps by default have all possible keys initialized with default values for their specified type. This means you cannot iterate over it. But if you have an array which holds all keys(owner addresses in our case) you can iterate over that array and achieve the same result.

This results in the ability to iterate over the owners in linear O(n) time and also have a constant O(1) time access.

Second Part (Constructor)

function SimpleMultiSig(uint threshold_, address[] owners_) {
    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_;
  }

The constructor initialises a new wallet contract.

First thing that happens is it makes sure the passed values are within the correct ranges and throws an error if requirements are not met. - owners are not more than 10 - threshold (num of confirmations) is not more than the number of owners - make sure there's a minimum of one confirmation

Next we iterate over the owners_ array and add each address to the contract's owner list. And finally set the threshold.

Third Part (transfer)

  // Note that address recovered from signatures must be strictly increasing
  function execute(uint8[] sigV, bytes32[] sigR, bytes32[] sigS, address destination, uint value, bytes data) {
    require(sigR.length == threshold);
    require(sigR.length == sigS.length && sigR.length == sigV.length);

    // Follows ERC191 signature scheme: https://github.com/ethereum/EIPs/issues/191
    bytes32 txHash = keccak256(byte(0x19), byte(0), this, destination, value, data, nonce);

    address lastAdd = address(0); // cannot have address(0) as an owner
    for (uint i = 0; i < threshold; i++) {
        address recovered = ecrecover(txHash, sigV[i], sigR[i], sigS[i]);
        require(recovered > lastAdd && isOwner[recovered]);
        lastAdd = recovered;
    }

    // If we make it here all signatures are accounted for
    nonce = nonce + 1;
    require(destination.call.value(value)(data));
  }

  function () payable {}
}

This is the heart of the contract where most of the action happens.

Transfer Process

Before we go into detail you should know what the process of a multisig transaction is (based on this contract):

  1. A transaction to transfer Y ether to X is created.
  2. It is then signed using the private keys of the owners. That creates a signature per each transaction signing which is made up of three values (V, R and S), these points are part of the underlying elliptic curve cryptography used by Ethereum and is outside this scope.
  3. Once the signed transaction and signatures are created they are passed to the contract's execute(...) function.

Validation checks

Inside the function there are a few initial checks making sure that the signatures are complete (all the V,R and S variables are present for each signature).

Handle the signed data

Then it creates the signed data following the ERC-191 standard proposal.

Verify signed data with signatures

The function then iterates for each number of required confirmations (threshold) and uses the ecrecover function to recover the public keys using the signatures. That public key is then compared against the owner's public key(address) and if it matches then you know that the owner has signed the transaction.

This way of verification allows you to verify that something has been sent/signed by a specific person without the need of knowing their private key.

When the minimum number of confirmation has been reached and successfully validated we move to the last part.

Nonce Increase & executing transfer

We increase the nounce counter for the next transaction (avoiding replay attacks).

Finally calling the .call.value(value)(data) to transfer the funds in a generic way.

Fallback function

It's important to notice that the fallback function() is declared with a payable() modifier. That allows the contract to accept ether payments.

P.S. One thing that I am unsure about it why the contract requires the addresses to be increasing with each consecutive address (the owners_[i] > lastAdd check).

| improve this answer | |
  • Thank you so much. This is an amazing explanation and you have helped me so much. As for the increasing address thing , "I belive the purpose is to facilitate checking against duplicate signatures, but it would be nice to make that explicit." according to a third party reviewer github.com/christianlundkvist/simple-multisig/blob/master/…. – Donely Mar 10 '18 at 8:00

Not the answer you're looking for? Browse other questions tagged or ask your own question.