Executing functions on other contracts with multisig wallets, or, multisig functions on Ethereum

TL;DR

Multisig wallets are typically used to handle ether and ERC20/ERC223 tokens, but they can also be used to interact with other smart contracts, including requiring that a function from another smart contract cannot be executed without multisig approval. By sending raw transaction data in the `data` input of a transaction, signatories can use multisig contracts to execute a function from a separate contract.

WARNING: We dive into Solidity code for multiple smart contracts. Some experience with Solidity will allow you to get the most from this article, but is not required to follow along.

Background

Why multisig wallets aren’t actually wallets

To refer to a multisig (multisignature) wallet as a “wallet” is technically a misnomer. In Ethereum, a multisig “wallet” is actually a smart contract. Both a smart contract and a wallet are identified with a unique address: an unsigned 256-bit integer. However, a wallet has a private key, which is required to sign transactions. A wallet’s address can be generated with its private key, which is why a wallet’s address is also referred to as its public key.

Because a smart contract lacks a private key (and is therefore unable to sign transactions), it does not implicitly have the ability to send ether. This means that, by default, if someone sends ether to the address of a smart contract (which is technically a valid address), the ether becomes inaccessible. Developers and investors alike have watched etherscan.io in horror as various smart contracts collect tokens, forever lost from the market. This is how the ERC223 proposal came about: it is a clever way to cause a transaction to fail if tokens are sent to a contract that can’t handle them. But this is only to prevent sending tokens to contracts that can’t handle them. If a smart contract is intentionally going to be used to handle ether or tokens, it needs to have that functionality written into it. Some basic ways to do accomplish this are through the selfDestruct() function for ether and token fallback function for tokens.

What multisig contracts do

Multisig contracts provide an alternate security measure for storing cryptocurrency by requiring multiple authorized signatories to confirm a proposed transaction before the transaction can execute. Weuse the word “alternate” because multisig contracts don’t reduce risk — they reduce a kind of risk and replace it with another, so the decision to use such a wallet is a strategy decision according use-case, not something that every crypto user necessarily needs.

Traditional use-cases for multisig contracts

The particular risk multisig contracts seek to alleviate is the loss of a private key. With wallets, if you lose access to your private key (including any backup seed, phrase, or password), you lose access to its assets. A multisig contract effectively allows multiple wallets to have authority over a single set of assets. If one private key is lost, remaining wallets can still access the set of assets. If the ownership of a private key is compromised, it alone does not have the authorization to unilaterally access the assets.

How to get ether from a multisig contract

When a new instance of the contract is deployed to the blockchain (depending on which multisig contract you use), you typically specify the public keys that represent which wallets will be authorized signatories, as well as how many of the signatories are required to authorize a proposed transaction. Any wallet can send ether to the contract, but a minimum number of signatories are required to get ether out of the contract.

Risk when using multisig contracts

The trade-off compared to using a traditional wallet is that you take on the risk that your tokens will stolen or permanently locked in the smart contract through hackers discovering an exploit in the contract code. See the Parity “wallet” (smart contract) hack. The code for previously-found exploits has been patched for subsequent smart contracts. Every new exploit acts as a learning experience for the Ethereum community. The mistakes of one contract become the education for a series of new contracts, including GNOSIS, ConsenSys, and Mist wallet. Multiple growing communities are at work to develop the robustness of these technologies. However, just because a new exploit hasn’t been found in newer multisig contracts doesn’t mean another doesn’t exist. If you are going to use a multisig contract, you must consider the use-case and its associated risk.

What we need from a multisig contract at Woolf

What if we don’t only want to use a multisig contract to handle ether transactions? What is we want certain functions to only be executable when approved by a certain number of signatories? Let me give you two examples:

  1. We want to transfer ERC20 or ERC223 tokens held by the smart contract with the same security measures afforded to a multisig contract handling ether.
  2. We want to override standard rules in a smart contract according to some network vote or in response to a legal requirement.

Both of these examples call for functionality that we at Woolf want to build into some of our smart contracts. For the first example: because we have an ERC223 token, we want those held in a multisig contract like ether. For the second example: part of our company organization involves an entity called the Woolf Reserve. The Woolf Reserve holds a large number of ERC223 tokens from the moment tokens begin to be sold. The Reserve makes available 0.035% of its token balance available to withdraw by an authorized address once per month to fund ongoing blockchain development. But what if we discover a bug in our smart contract? We would need to withdraw the entire token balance at once and move it to a new contract version of the Reserve. We call this function forceWithdraw(). But we don’t want anybody to be able to call this. Typically in Solidity, we use the adminOnly() or onlyOwner() modifier for this. But we also don’t want a single person within the organization to have the power over all those tokens. What we need is for multiple pre-approved signatories and no one else to be able to call this function upon approval, where signatories are separated by business domain: core leadership, management, and legal authority.

How to execute multisig contract functions

I’ll use GNOSIS’s MultiSigWallet as an example. We’ll look at the following functions:

addOwner()
removeOwner()
replaceOwner()
changeRequirement()
submitTransaction()
confirmTransaction()
revokeConfirmation()
executeTransaction()

submitTransaction(), confirmTransaction(), revokeConfirmation(), and executeTransaction() functions can be called by an authorized signatory. addOwner(), removeOwner(), and replaceOwner() can only be executed if the sending address is the multisig contract itself, through the modifier, onlyWallet(), which functions like onlyOwner(). This is a protection mechanism. If one private key was compromised and the individual had authority to add, remove, and replace owners unilaterally, much of the purpose of a multisig contract is defeated. The only way to call a function from the contract itself is for a signatory to propose it by calling submitTransaction().

How to use submitTransaction()

The submitTransaction() function takes three parameters: destination (type address), value (type uint), and data(type bytes). When you’re simply trying to send ether from the smart contract to some address, you enter the address you want to send ether as to, then how much you want to send (denominated in wei) for value, then leave the data section empty. But if you’re trying to execute a function from the multisig contract, like addOwner(), it’s a different process.

To execute a function of the multisig contract that has the onlyWallet() modifier, the to address must be the contract itself. Think of this as an individual submitting a proposal to the contract. The value (of ether) is 0 or left blank. The data section is where things get interesting. There, you input function information in the form of hex code. Think of this section as the thing that gets executed when a transaction is approved by the required signatories. If you want that function to be addOwner(), then the hex code of the addOwner() and associated parameters is submitted within the data section. The same is true for removeOwner() and any other function with the onlyWallet() modifier.

In this example, we will submit a new transaction to add an owner (signatory). Here’s what that looks like:

to: 0x0000000000000000000000000000000000000000000000000000000000000001 // multisig contract address
amount: 0 // amount of ether
data: 0x7065cb480000000000000000000000000000000000000000000000000000000000000002 // addOwner(address owner)

You may be wondering about what’s going on with the data section. It’s the hash of the addOwner() function, including the parameter being passed to it (in this case, an address to add).

0x7065cb48 = hash of addOwner()

0000000000000000000000000000000000000000000000000000000000000002 = the address 0x0000000000000000000000000000000000000002 with the 0x removed then converted to a 256-bit value.

This function is being proposed by an existing owner, or signatory, using submitTransaction(). Other owners use confirmTransaction() to confirm the proposal by passing in the automatically-generated ID associated with the proposed transaction as a parameter. The minimum number of required owner approvals, as set in the constructor, must confirm the proposal before the proposed transaction (the data portion) will be executed. Once the minimum has been met, any owner can call executeTransaction(). This will cause the multisig contract to execute the proposed transaction, including what was in the data section.

Note: executing the transaction proposed to the multisig contract causes msg.sender to be the contract address, not the address of any of the contract owners (or signatories). This is crucial for executing functions in separate contracts with regard to contract ownership. More on that later.

How to get the hash of a function that you want to propose to a multisig contract with submitTransaction()

Before you can propose a transaction in the form of a hash, you need to know what that hash is. I’ve found that the easiest way is to use a website like MyEtherWallet. This may seem like a lot of steps, but it becomes a relatively rapid process after you understand the workflow.

From MyEtherWallet, Choose the appropriate network you want to use (i.e. MainNet; a test network like Rinkeby or Ropsten, etc.).

Click the Contracts tab and make sure you are on the Interact with Contract sub-tab. Type in the contract address of your multisig contract. Ignore “Select Existing Contract”, then type in the ABI / JSON Interface of the contract.

Note: An easy way to get the ABI / JSON Interface is to use Remix, the Solidity IDE. In Remix, from the Compile tab, paste your code, click Start to compile, then click Details. A window will pop up.

Scroll down to the section labeled ABI. Copy that value to the clipboard and paste it into corresponding input on MyEtherWallet.

Once you have inputted the contract address and the ABI on MyEtherWallet, click Access to see available functions for the contract.

Select from the dropdown menu the function you want to propose, then type in the values for any parameters you want to submit with the function, like an address, string, uint256, etc.

Select how would you like to access your wallet, e.g. MetaMask / Mist, Ledger Wallet, TREZOR, etc. The wallet you use doesn’t matter, you just need to go through these steps. You won’t need to actually execute the transaction. You will cancel the transaction procedure at the very last step and won’t need to spend gas.

Click Write.

In the window that opens, leave Amount To Send as 0. Set a gas limit like 300000 so you get past any warning in MyEtherWallet, then click Generate Transaction. This doesn’t execute the transaction yet. It first shows the transaction data for the Raw Transaction and Signed Transaction. Scroll through the content of Raw Transaction and copy the content associated with the key data.

"nonce":"0x4c",
"gasPrice":"0x098bca5a00",
"gasLimit":"0x0493e0",
"to":"0x100230E5Fe16dcfEDCA9027D65E5Be182530e722",
"value":"0x00",
"data":"0x7065cb480000000000000000000000000000000000000000000000000000000000000002",
"chainId":4

In the above example, the data portion to copy is: 0x7065cb480000000000000000000000000000000000000000000000000000000000000002

Don’t select Yes, I am sure! Make transaction. Exit instead. You only needed to copy the data.

You now have the data necessary to include when submitting a transaction. By including it in the submitTransaction()‘s data section, you can execute that function through the multisig contract.

Submitting the transaction with MyEtherWallet

You don’t have to use MyEtherWallet for submitting a transaction, but if you do, here are the remaining steps to carry it out:

  1. Still on the Contracts tab, choose the submitTransaction() function from the dropdown below Read / Write Contract.
  2. Set the multisig contract address as the destination, 0 as the value, then paste the copied data for data. We are actually going to submit a transaction this time.
  3. Select write
  4. Select Generate Transaction.
  5. After reviewing the transaction data, select Yes, I am sure! Make transaction. This may prompt a further confirmation from the wallet you are using.

How to execute functions on other contracts through a multisig contract

Follow the previous steps for executing a function as part of the multisig contract, but with the following changes:

  • Instead of the multisig contract address as the to parameter, make it the contract you want to call a function from in a multisig manner.
  • Instead of using the hash of the multisig contract function as the data parameter, make it the hash of the function you want to call from an external function, specifically, of the contract address you just used as the to parameter.

Just like using MyEtherWallet to get the data from multisig contract functions to use in the data parameter of submitTransaction() (like for addOwner()), you can also use it to get the hash of the function you want to call from an external function. Load that contract’s address and ABI, then follow the steps to get the get the hash of the function and function arguments.

Now, signatories confirm the transaction with the corresponding internally-generated ID. This process allows multisig contracts to execute functions of other contracts.

Securing contract functions with a multisig contract.

onlyOwner() from OpenZeppelin is a popular modifier that prevents functions from being executed unless the address of msg.sender is the owner. Because msg.sender of a multisig contract is the contract address, we can use this to make the contract owner not a typical wallet, but the multisig contract itself. This way, the function cannot be called unless it is first proposed, confirmed, and executed through a multisig contract, like how the modifier onlyWallet() is used within the multisig contract.

Because only wallets can sign transactions, you cannot use a multisig contract to create a contract. You can, however, set the multisig contract address as the owner at the instantiation of some other contract. If you want to change the existing owner of a contract, you can call transferOwnership() and set the address of the new owner as the multisig contract address.

Bringing it all together

By sending raw transaction data in the data input of a transaction, signatories can use a multisig contract to execute a function from a separate contract, offering an alternative security measure or business strategy decision according to your organization’s needs.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store