Protect Your ETH

This application allows you to create and spend from an Ethereum 2/3-multisig smart contract controlled by hardware wallets.

It was built by Unchained Capital to help holders worldwide safely and collaboratively store their ETH using multisig cold-storage provided by hardware wallets.

If you are storing ETH on a Trezor or Ledger and you trust at least two other people, you could be doing it better. You can use this application with your trusted friends, family, and colleagues to protect your wealth.

To learn more about the design philosophy of this application and smart contract, read the blog post. If you are a developer you may want to check out the source code for the smart contract (which includes this dApp) on GitHub. This contract has been fully audited by Hosho and you can read the audit report yourself. Unchained Capital is also operating a bug bounty around this contract & dApp.

Useful Links
Blog Post
GitHub Repository
Hosho Audit
Full Report
Suggest an improvement
GitHub Issues
Report a bug?
Bug Bounty

Hardware Wallets Required

This application connects hardware wallets with the Ethereum blockchain.

When creating the multisig smart contract, three hardware wallets are required. When spending, 2/3 of these same hardware wallets are required. The private keys used to create and spend from a contract are only present on your hardware wallets. This application stores no data whatsoever about you, your wallets, your addresses, or your transactions.

This means that you are completely in control of your own security and accounting. In particular:

  • You must yourself track the addresses of the smart contracts this application creates.
  • For each such address, you must track which harware wallets you used to create the address, along with which BIP32 derivation path you used with each hardware wallet (it is OK to reuse the same BIP32 path with each wallet).
  • You must protect these hardware wallets and their corresponding wallet words. 2/3-multisig is a great security improvement over single-signature but that's no excuse to get sloppy about the security of an individual key.
Supported Devices

This application has been tested with the following hardware wallets:

Ethereum Client Required

This application requires a funded account in a synced, connected Ethereum client to pay for gas.

The Ethereum transactions required to create and spend from the smart contract will be authored by this application. Broadcasting them to the Ethereum network requires a synced, connected Ethereum client. The client should have an account with some modest ETH balance to pay for gas. As of this writing, 0.01 ETH is more than sufficient.

The account used to pay for gas has no rights to fund, spend from, or otherwise control the smart contract this application creates. This means creating and spending from this smart contract can be done by separate accounts, as long as this application and the same 3 hardware wallets are used.

We recommend you use the Metamask plugin. The design of the smart contract allows using a convenient browser-based client such as Metamask without compromising security.

Additional Clients

The following clients may also be used, though they may require some special configuration or browser extensions.

See the dApp section of the README for this application's GitHub repo.


Unchained Capital is actively developing this project and very much welcomes feedback. To file bugs or request features, please use GitHub issues.

Unchained Capital is also operating a bug bounty for this project.

Source Code

The source code of the smart contract used by this version of this application is below. You can also find it on GitHub or see it on Etherscan for any contract created by this application.

pragma solidity ^0.4.24;

// A 2/3 multisig contract compatible with Trezor or Ledger-signed messages.
// To authorize a spend, two signtures must be provided by 2 of the 3 owners.
// To generate the message to be signed, provide the destination address and
// spend amount (in wei) to the generateMessageToSignmethod.
// The signatures must be provided as the (v, r, s) hex-encoded coordinates.
// The S coordinate must be 0x00 or 0x01 corresponding to 0x1b and 0x1c
// (27 and 28), respectively.
// See the test file for example inputs.
// If you use other software than the provided dApp or scripts to sign the
// message, verify that the message shown by the device matches the
// generated message in hex.
// WARNING: The generated message is only valid until the next spend
//          is executed. After that, a new message will need to be calculated.
// ADDITIONAL WARNING: This contract is **NOT** ERC20 compatible.
// Tokens sent to this contract will be lost forever.
// 1: Invalid Owner Address. You must provide three distinct addresses.
//    None of the provided addresses may be 0x00.
// 2: Invalid Destination. You may not send ETH to this contract's address.
// 3: Insufficient Balance. You have tried to send more ETH that this
//    contract currently owns.
// 4: Invalid Signature. The provided signature does not correspond to
//    the provided destination, amount, nonce and current contract.
//    Did you swap the R and S fields?
// 5: Invalid Signers. The provided signatures are correctly signed, but are
//    not signed by the correct addresses. You must provide signatures from
//    two of the owner addresses.
// Developed by Unchained Capital, Inc.

contract MultiSig2of3 {

    // The 3 addresses which control the funds in this contract.  The
    // owners of 2 of these addresses will need to both sign a message
    // allowing the funds in this contract to be spent.
    mapping(address => bool) private owners;

    // The contract nonce is not accessible to the contract so we
    // implement a nonce-like variable for replay protection.
    uint256 public spendNonce = 0;

    // Contract Versioning
    uint256 public unchainedMultisigVersionMajor = 2;
    uint256 public unchainedMultisigVersionMinor = 0;

    // An event sent when funds are received.
    event Funded(uint newBalance);

    // An event sent when a spend is triggered to the given address.
    event Spent(address to, uint transfer);

    // Instantiate a new Multisig 2 of 3 contract owned by the
    // three given addresses
    constructor(address owner1, address owner2, address owner3) public {
        address zeroAddress = 0x0;

        require(owner1 != zeroAddress, "1");
        require(owner2 != zeroAddress, "1");
        require(owner3 != zeroAddress, "1");

        require(owner1 != owner2, "1");
        require(owner2 != owner3, "1");
        require(owner1 != owner3, "1");

        owners[owner1] = true;
        owners[owner2] = true;
        owners[owner3] = true;

    // The fallback function for this contract.
    function() public payable {
        emit Funded(address(this).balance);

    // Generates the message to sign given the output destination address and amount.
    // includes this contract's address and a nonce for replay protection.
    // One option to independently verify:
    // and select keccak
    function generateMessageToSign(
        address destination,
        uint256 value
        public view returns (bytes32)
        require(destination != address(this), "2");
        bytes32 message = keccak256(
        return message;

    // Send the given amount of ETH to the given destination using
    // the two triplets (v1, r1, s1) and (v2, r2, s2) as signatures.
    // s1 and s2 should be 0x00 or 0x01 corresponding to 0x1b and 0x1c respectively.
    function spend(
        address destination,
        uint256 value,
        uint8 v1,
        bytes32 r1,
        bytes32 s1,
        uint8 v2,
        bytes32 r2,
        bytes32 s2
        // This require is handled by generateMessageToSign()
        // require(destination != address(this));
        require(address(this).balance >= value, "3");
                v1, r1, s1,
                v2, r2, s2
        spendNonce = spendNonce + 1;
        emit Spent(destination, value);

    // Confirm that the two signature triplets (v1, r1, s1) and (v2, r2, s2)
    // both authorize a spend of this contract's funds to the given
    // destination address.
    function _validSignature(
        address destination,
        uint256 value,
        uint8 v1, bytes32 r1, bytes32 s1,
        uint8 v2, bytes32 r2, bytes32 s2
        private view returns (bool)
        bytes32 message = _messageToRecover(destination, value);
        address addr1 = ecrecover(
            v1+27, r1, s1
        address addr2 = ecrecover(
            v2+27, r2, s2
        require(_distinctOwners(addr1, addr2), "5");

        return true;

    // Generate the the unsigned message (in bytes32) that each owner's
    // wallet would have signed for the given destination and amount.
    // The generated message from generateMessageToSign is converted to
    // ascii when signed by a trezor.
    // The required signing prefix, the length of this
    // unsigned message, and the unsigned ascii message itself are
    // then concatenated and hashed with keccak256.
    function _messageToRecover(
        address destination,
        uint256 value
        private view returns (bytes32)
        bytes32 hashedUnsignedMessage = generateMessageToSign(
        bytes memory unsignedMessageBytes = _hashToAscii(
        bytes memory prefix = "\x19Ethereum Signed Message:\n64";
        return keccak256(abi.encodePacked(prefix,unsignedMessageBytes));

    // Confirm the pair of addresses as two distinct owners of this contract.
    function _distinctOwners(
        address addr1,
        address addr2
        private view returns (bool)
        // Check that both addresses are different
        require(addr1 != addr2, "5");
        // Check that both addresses are owners
        require(owners[addr1], "5");
        require(owners[addr2], "5");
        return true;

    // Construct the byte representation of the ascii-encoded
    // hashed message written in hex.
    function _hashToAscii(bytes32 hash) private pure returns (bytes) {
        bytes memory s = new bytes(64);
        for (uint i = 0; i < 32; i++) {
            byte  b = hash[i];
            byte hi = byte(uint8(b) / 16);
            byte lo = byte(uint8(b) - 16 * uint8(hi));
            s[2*i] = _char(hi);
            s[2*i+1] = _char(lo);
        return s;

    // Convert from byte to ASCII of 0-f
    function _char(byte b) private pure returns (byte c) {
        if (b < 10) {
            return byte(uint8(b) + 0x30);
        } else {
            return byte(uint8(b) + 0x57);

This application is in “alpha” state and is presented for evaluation and testing only. It is provided “as is,” and any express or implied warranties, including but not limited to the implied warranties of merchantability and fitness for a particular purpose, are disclaimed. By using this application, you accept all risks of such use, including full responsibility for any direct or indirect loss of any kind resulting from the use of this application, which may involve complete loss of any ETH or other coins associated with addresses used with this application. In no event shall Unchained Capital, Inc., its employees and affiliates, or developers of this application be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this application, even if advised of the possibility of such damage.