Multi-signature wallets are smart contracts that require the agreement of multiple people to perform an action. They can be useful for protecting assets (using separation of duties) or to ensure that certain actions are only taken in accordance with the wishes of the multisig’s owner or a majority of owners.
This article focuses on how to make the best design choices when setting up a multisig and how to avoid common mistakes. We’ll walk through demos of several different multisig configurations. Once you’ve set up your multi-signature wallet, you can add it to your application.
Jump ahead:
There are many cases in which we want actions to be approved by multiple people. Here are a few examples:
If an asset is owned by multiple people in common, especially an on-chain asset, a smart contract can verify that it is used only in accordance with the wishes of the owners. The blockchain also provides an audit trail that shows which owners approved any action, so it is impossible for owners to later pretend they did not approve.
Even when an asset is owned by a single entity, multisigs can be useful for implementing separation of duties. When multiple people are required to sign off on an action both fraud and innocent mistakes are a lot less likely. In these cases, the tradeoff is between security (more signers mean you’re safer) and speed (more signers mean it takes longer to do anything).
There are cases where multiple people are allowed to perform an action, and we just want to know who executed the action. By using a multisig that requires only a single signature, we can cover this use case without the security risks associated with a shared account.
Entities on the blockchain, such as a multisig contract, can only directly affect other blockchain entities. The actions that a multisig can control are therefore those that can be accomplished by calling a smart contract, such as transferring ERC-20 tokens or an NFT.
Multisigs have multiple signature addresses that are authorized to perform an action, either individually or when approved by a group of a specific size. Every signature address is a different Ethereum address, typically derived from a different recovery phrase and owned by a different person. Later in this article, we’ll discuss circumstances in which you may want to give a single person control of more than one signer address.
Most multisigs implement an M-of-N requirement. This means that there are N total signers, of which M have to approve and sign before the action occurs. This is called an M/N multisig; the ratio of M to N is called the quorum quotient. For example, a 3/5 multisig would have five signers, three of whom would need to agree to or approve an action.
The tradeoffs in setting up the parameters of a multisig come down to tradeoffs between security on one hand and ease and availability on the other.
To learn more about the multisig quorum quotient and compare different cases, let’s create a wallet for a company with four managers. In our example, the multisig will need to be accessed to change a greeting. We’ll look at three configurations: no multisig, 1/3 multisig, and 2/4 multisig.
Of course, the purpose of this example is simply to demonstrate the multisig, not the contract it controls. In real-world applications, contracts generally perform more valuable functions than changing a greeting and they generally limit the number of individuals who can make a change.
Before we actually get to the multisig we should set up our lab environment and target contract (the contract the multisig controls). The lab environment runs on top of the Goerli test network. If you need Goerli test ETH, you can get it at this faucet.
For our demo, we’ll use a simple smart contract called Greeter.sol
, which I deployed with Hardhat. You can see it here.
To see the current greeting, open Contract > Read Contract and then expand greet.
To modify the current greeting, open Contract > Write Contract. Then, click Connect to Web3 to connect to the wallet. After selecting a wallet from the listed options, click setGreeting and type the new greeting. Then, click Write and approve the contract in the wallet.
Note that due to caching, after you change the greeting you may need to reload the contract a few times before you’re able to see the new greeting.
The demo multisig was created with Gnosis Safe, which is probably the most common multisig platform.
The addresses that are authorized to use the multisig are all derived from the passphrase: “dumb cart rally entry iron flock man demise record moon erode green”
The addresses are as follows:
In real-world scenarios, the addresses come from unique passphrases when they belong to different people. However, doing that here would require you (as the reader) to continually log out of one passphrase and into another or to use multiple devices. For this training, I’ve decided that convenience outweighs security, so we’ll omit the unique passphrases in this demo.
Now, let’s look at an example in which only the owners can change the greeting. In this example, just one signature is required to make a change.
We are going to use the same Greeter.sol
contract. In a real-world application, we’d probably implement Ownable
and set the owner to the multisig, but the purpose here is to make things as simple as possible, not as secure as possible.
When a single signer is required, you need to propose and then confirm the transaction.
0x8A470A36a1BDE8B18949599a061892f6B2c4fFAb
setGreeting
method and type a new greetingNext, let’s look at an example in which two of the four owners must sign. For this demo, we’ll need to pretend to be a second manager and approve the transaction in order to have the two signatures needed for the transaction to occur.
First, follow the steps in the previous example, but use this safe.
Now, view the transaction, and then verify that the requested action occurred (that the greeting really did change):
Multisig wallets are meant to provide additional security, but issues can still arise. Let’s look at some examples.
The great advantage of the blockchain is that there is no central authority. In the example above, no one can approve a transaction from the multisig except for at least two of those four manager addresses.
The great disadvantage of the blockchain is that there is no central authority to override contracts in times when it is justified. For example, in the case of the death of three signers of a 2/4 multisig, there would be no way for the multisig to release any of its assets. The wallet’s assets would remain locked forever.
One option to provide a backup for this type of scenario is to have someone the company trusts completely (e.g., the owner) generate two additional addresses and store their passphrases in tamper-resistant envelopes in a secure location. An off-premise location, such as the safe of the company’s attorney or account, is often a good option.
In a multisig, all signers are equal. The problem is that sometimes we want signers who are more equal than others. For example, we might want the business managers to be able to do something with an additional signature, but for the owner to be able to do anything.
One solution would be to allow the owner’s address to access the target contract directly, without going through the multisig. This solution has the best usability, but it means we cannot fully rely on the multisig for auditing.
A second option is for the owner to generate two addresses from the passphrase and use both addresses as signers. This solution has more limited usability but could be a better option if part of the purpose of the multisig is to reduce the chance of a careless mistake and if owner overrides are to be used as an emergency measure, rather than part of daily processing.
Now, let’s look at a more complex scenario, one in which two companies collaborate and the wallet’s function requires approval from at least one manager from each company.
Because all signers are equal in a multisig, we need to write some logic into the contract in order to achieve this goal. Click here to see the Solidity contract.
Let’s see what happens when company A proposes a new greeting.
0x3e55E2DBDE169Fbf91B17e337343D55a7E0D728e
Next, let’s see what happens when company B proposes a different greeting. This step is necessary because it’s not enough to see that the smart contract behaves correctly when people follow proper procedures. It is just as important to ensure that the contract remains secure when people do not follow a proper procedure.
0x3e55E2DBDE169Fbf91B17e337343D55a7E0D728e
Now, let’s see what happens when company B proposes the same greeting that was proposed by company A.
Let’s look at the Solidity code to see how this works:
/** *Submitted for verification at Etherscan.io on 2022-05-08 */ //SPDX-License-Identifier: Unlicense pragma solidity ^0.8.0; contract AB_Greeter { string greeting;
Here are the addresses of the multisigs:
address multisigA; address multisigB;
These variables hold the hashes of the proposed greetings.
Using the hashes has two advantages.
If we were to store strings they could be much longer and more expensive. Also, Solidity does not have an inbuilt expression to compare strings, so the easiest way to compare two strings is to compare their hashes. By using hashes, we only calculate the hash once for every time we call proposeGreeting[AB]
.
bytes32 proposedGreetingA = 0; bytes32 proposedGreetingB = 0;
To get started, we need the greeting, as well as the addresses of the two multisigs:
constructor(string memory _greeting, address _multisigA, address _multisigB) { greeting = _greeting; multisigA = _multisigA; multisigB = _multisigB; }
The functions greet
and setGreeting
are the same as in the Greeter.sol
contract we used earlier.
function greet() public view returns (string memory) { return greeting; } function setGreeting(string memory _greeting) internal { greeting = _greeting; }
This is the function to propose a new greeting.
function proposeGreetingA(string calldata _greeting) public {
Only multisigA
is allowed to propose greetings as company A; any other source will be rejected.
require(msg.sender == multisigA, "Only for use by multisig A"); bytes32 _hashedProposal = keccak256(abi.encode(_greeting));
If company B has already proposed what company A is proposing now, we update the greeting like so:
if(_hashedProposal == proposedGreetingB) setGreeting(_greeting);
Otherwise, we register this as company A’s proposed greeting:
else proposedGreetingA = _hashedProposal; }
It’s important to realize that this isn’t the ideal way to accomplish this goal because multisigA
is a 1/3, so any of company A’s managers could switch the multisig and take away the other two signers’ ability to propose or approve anything.
A more sensible policy would be to have another multisig, maybe a 2/3, for this type of sensitive operation. However, the purpose of this example is to teach, so we’ll opt for simplicity over security.
In the code below we specify that multisigA
can switch to a new multisig if that is ever needed.
function changeMultisigA(address _newMultiA) public { require(msg.sender == multisigA, "Only for use by multisig A"); multisigA = _newMultiA; }
Company B’s functions are the mirror image of those of company A.
function proposeGreetingB(string calldata _greeting) public { . . . } function changeMultisigB(address _newMultiB) public { . . . } }
Smart contract development is relatively easy, but secure smart contract development is not. Unless you have a lot of security expertise it is highly recommended that you have someone knowledgeable review your logic and code before trusting it in a mission-critical application.
For example, when I wrote the AB_Greeter
contract, I first used just a single variable for the proposed greeting, and my code looked like this:
function proposeGreetingA(string calldata _greeting) public { require(msg.sender == multisigA, "Only for use by multisig A"); bytes32 _hashedProposal = keccak256(abi.encode(_greeting)); if(_hashedProposal == proposedGreeting) { setGreeting(_greeting); } else { proposedGreeting = _hashedProposal; } }
Can you spot the problem?
Two approvals are indeed required to change the greeting. However, company A can just call proposeGreetingA
twice with the same greeting. The first call puts the hash of the new greeting as the proposal. The second call sees that the new greeting’s hash is identical to the proposal and updates the greeting.
If the proposal had come from company B this would have been fine, but here the proposal came from company A, so this is a violation of the terms.
To resolve this issue, I decided to use two separate proposals, one under company A’s control and the other under company B’s control.
I am not saying that the logic in the current contract is 100 percent secure. If I were to use this in production I’d ask some other people to look at it first. Smart contracts exist to enable trustless cooperation. When you write them, you have to assume they will be used in a hostile environment. The expense of running a smart contract instead of a more conventional program is only justified if the environment is potentially hostile.
Multisigs are a simple solution to a simple problem – how to get permissions from a group when all group members are equal and group membership rarely changes.
In this article, we reviewed some mechanisms to extend this functionality, either by using the multisig in an unusual way (the owner with two signers) or by adding our own logic in a separate smart contract (the two company scenario).
If your signer population is dynamic, or if you have many different roles, each with its own permissions, a multisig may not be the ideal solution. Instead, a decentralized autonomous organization may be a better option.
However, if the business requirements you need to implement are such that a multisig is sufficient, this is a much simpler solution than creating a DAO. Notice in our first example we didn’t need to write any code. You can also integrate multisigs into your own applications using the SDK.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]