Matej Šima

@matej.sima

Implementing a Multi-Sig Smart-Contract in Tezos; using ReasonML

This article will walk you through setting up a smart-contract development environment for tezos. While explaining how to design and implement a lean multi-signature smart contract using ReasonML & Liquidity.

Prerequisite

This article presumes at least an entry-level understanding of how blockchain technologies work. We will use a sandboxed Tezos network for development of our smart contract, but the same process applies to other networks such as Alphanet or Mainnet. Let’s get started!

🚨 Disclaimer
The following contract shall be used for educational purposes only, it has not been audited or reviewed, please use with caution.
If you want to fast forward a bit, you can find the final smart contract’s source here. 🏎

What is multi-sig?

Multi-signature is a paradigm, used to divide responsibility over funds. In our case, we want two users, to have shared control over their ꜩ (tezzies — native currency of Tezos).

Setting up the sanboxed environment

To develop our smart contract, we’ll start by setting up the tezos-environment-manager.

You can read a more detailed introduction to tezos-environment manager here.👈

Setup is pretty straightforward:

  1. Clone the repository
  2. Choose an environment
  3. Build the images
  4. Start the node, it takes roughly 30 seconds to start
  5. Start the client
Please be patient while your docker images are built, in a future release, those images will be available at hub.docker.com
Setting up tezos-environment-manager

Designing our smart contract

To achieve multi-sig capabilities, we first need to know who controls the account (contract). In our case, it should be two different accounts (a.k.a. wallets). Our sandboxed environment already provides a handful of accounts available, they’re aliased as bootstrap1 up to bootstrap5.

Account aliased as bootstrap1 and it’s keys
Account aliased as bootstrap2 and it’s keys

Our owners shall be bootstrap1 & bootstrap2, which in practical terms, means that those accounts (private keys) are usually held by two separate people — who can then engage in a multi-sig transaction together via the smart contract we’re developing.

⚠️ Our smart contract is written in ReasonML, but it’s transpiled to Liquidity, and from there it’s compiled to Michelson

Entry points

We’ll expose four entry points in our contract: create_proposition, sign_proposition, delete_proposition, execute_proposition. We’ll walk through them as we progress in our tutorial.

Flow should go as the following:

  1. Propose a new transaction
  2. Sign the proposed transaction with all contract owners (individual entry point calls)
  3. Execute the proposition, once it’s signed by every owner.

Optionally, you can delete a proposition half-way through the process, in order to be able to create a new one instead.

The storage

Smart contracts can store data on the chain, via their storage, which is defined as a type, and initialized with default values before deployment.

This allows us to deploy countless multi-sig contracts, which are owned by different combinations of accounts.

Our storage will consist of the following attributes:

  1. Set of owners — basically ‘an array’ of addresses, that can operate our smart contract
  2. Proposition — A proposed transaction by one of the contract owners, denoting what amount and to whom it should be sent. (destination, amount)
  3. Set of owners, who signed the existing proposition — Again ‘an array’ of accounts, which are the owners of the smart contract, who have signed a proposed transaction, after it was successfully proposed in the contract.
🔬 On a side note — each contract must specify version of the Liquidty language it uses, so let’s do that right at the top of our file

Let’s start coding by creating a file under src/contracts and we can call it multisig.re. All the contract related code below should end up in our contract file.

Each contract specifies the Liquidity version it’s written in at the top
Contracts can store data in storage, which is of a certain type.

For the sake of simplicity, let’s initialise our storage right away as well. We’ll use bootstrap1 & bootstrap2 to set the owners in our storage. And we’ll leave out the other storage attributes ‘blank’, so a variant None and an empty Set.

Storage can be initialised, in our case with predefined owner addresses

ACL — Access Control Lock

Our smart contract needs to limit access to certain entry points. We can do that by comparing the address of the person, who called our contract via a transaction, with address of owners that we’ve specified as initial storage.

Let’s define a method that we can re-use, we’ll call it can_call, and it will accept a single parameter, which is the current storage value. Additionally, we’ll implement a helper method fail_with_wrong_ownership that makes the current contract transaction fail — we’ll use it when can_call evaluates to false.

Proposing a multi-sig transaction

To propose a new transaction within our multi-sig contract, we’ll use a method called create_proposition, which takes two arguments, one is of type proposition and the second one is of type storage.

Flow of this method is relatively straight forward, firstly check if the current transaction was initiated by one of the owners, if not — fail because of ownership issues. In case an actual multi-sig contract owner is calling this entry point, check if there is an existing proposition already, if there is one, fail because we don’t want to override it and loose it’s data. If there’s no existing propositions, save our new proposition as the current one.

Deleting a multi-sig proposition

It’s always nice to have a way out, in our case, we allow the predefined owners to delete a pending proposition, prior to creating a new one. There’s no parameter for our delete_proposition entry point, hence we use unit.

Again we check who’s calling the contract entry point, if our ACL passes, we delete the existing pending proposition, if not — we abort the operation.

Signing a multi-sig proposition

In order to execute the proposed transaction, we must sign it first. We’ll have an entry point available to do just that, and it will be called by each owner independently to provide a proposition signature unique to the owner.

First we check the ACL as we did in delete_proposition, if it passes, we’ll add a signature based on the Current.sender() → which is the address of the account which called our entry point in a transaction — and that’s one of the contract owners.

Set.add only adds values to the set if they are unique, that means if you call the sign_proposition entry point multiple times, with the same owner signing the contract call transaction, you won’t create a second signature, as the addresses in our Set are unique.

Executing a multi-sig proposition

After all the owners have signed the proposition, we can proceed by executing the proposition.

We begin by verifying our ACL, if it passes, we check if the number of existing signatures, is matching the number of multi-sig contract owners. If we have sufficient (100%) signature coverage, we proceed by creating an Account.transfer transaction, with destination & amount taken from our proposed transaction.

We end the entry point execution by returning a cleaned up storage thanks to delete_proposition helper function defined earlier, that resets our storage back to the initial state with no existing proposition & signatures. And we specify a transaction/operation to be executed as a result of our call — which is the proposed transaction to the destination account with a given amount.

🎉 Congratulations, your smart contract is now ready to be deployed into the blockchain! 🎉

You can find the full contract’s source here.

Deploying our smart contract

We’ll now go back to our client shell, that we’ve started at the beginning by running make client.

Firstly we have to transpile/translate our ReasonML code, to Liquidity code, we can do that using refmt — a tool used to convert ReasonML to OCaml and vice versa, and since Liquidity is a subset of OCaml, we can use it for our contract as well.

Converting ReasonML to Liquidity
Now we’ve got a file called multisig.liq, you can check it out with your editor, syntax should be OCaml, so don’t forget to configure your editor of choice if it doesn’t recognize .liq straight away

Forging a deployment operation

We will now forge a deployment operation that we can sign. Before we do so, let’s examine what exactly will go down.

We’re using Liquidity’s CLI, with our local sandboxed node as $NODE_URL

Amount of 1000tz will be consumed by our contract, when executing proposed transactions.

Specified fee of 1tz, serves as an incentive for the bakers to include our operation in a new block.

Source is the address of an account that’s facilitating the deployment — in our case it’s bootstrap1, who also happens to be an owner of the multi-sig contract and a baker in our sandboxed environment.

Signing the deployment operation

We’ve now got operation bytes to sign, so let’s do that by running the following command. We’ll sign as bootstrap1.

Make sure to save the resulting signature, we’ll use it in the next step.

Injecting the deployment operation with a signature

Next step in the deployment process is to inject an operation, using the bytes & signature generated earlier.

Please note that your generated signature may vary, make sure to copy the real signature to the command below.

Looking up our contract in TzScan

At this point, our contract is not yet included in a new block, because we haven’t baked a new block ourselves yet. Let’s make sure we can’t see our contract yet on TzScan, by visiting http://localhost:8000/contracts in your browser.

No contracts available at this time

Baking a new block to include our deployment operation

In order to finish the deployment process in our sandboxed environment, we must bake a new block so the deployment operation is included.

New block has been baked, including our smart contract.

🎉 You can now check TzScan again, and you’ll see our multi-sig contract deployed 🎉

Contract has been deployed successfully

Talking to our smart contract

Before we begin interacting with our smart contract, let’s check out how it looks like on TzScan.

We can see the current storage status of our contract, and right now there’s nothing else but our predefined set of owners — bootstrap1 & bootstrap2.

Storage is initialised correctly
There are no transactions yet, and the balance is 1000tz

Creating a multi-sig proposition, signed by an unrightful owner

One of the possible interactions with our smart contract, is to call one of the entry points using an account, that’s not listed as an owner in our contract.

Let’s see how that ends up, we’ll use bootstrap3 account from our client, which is not used anywhere in our contract.

Retrieving the keys for bootstrap3

Then we copy our deployed smart contract’s address from here:

In my case, the contract’s address is KT1Fm94xt2ujgtpYSJKvc54thx8Aq5pk7RkK.

We can now use bootstrap3‘s keys and our smart contract address to perform a contract entry point call. We’ll be proposing a transaction to bootstrap4tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv with amount of 0.01tz.

⚠️ When calling create_proposition we also have to specify the parameter, in this case our new_proposition. Syntax used to specify the parameter value is Liquidity/Ocaml.
Failed contract call

This call will inevitably fail, because that’s how our ACL logic is set up — don’t allow anyone, except the predefined owners, to create transaction propositions.

Creating a multi-sig proposition, signed by a rightful owner

If we sign our call using bootstrap1‘s keys, we’re able to call the contract successfully and create a proposition.

Successful contract call

For our contract call to take effect, we must bake a new block in our sandboxed environment again: tezos-client bake for bootstrap1.

Baking a new block to include our contract call

We can verify that our call/transaction was included in the chain, using TzScan:

Transaction has been included
Storage has been updated with the proposed transaction

Signing the proposition by contract owners

Before we can execute our proposed transaction, we must sign it by both of the contract owners, let’s call sign_proposition in two separate calls with different owners to sign and seal our proposition. We’ll use bootstrap1 and bootstrap2 to sign our proposition.

👉 If you’re feeling adventurous, you can try calling sign_proposition using an account, that’s not specified as an owner. It should fail thanks to our ACL restrictions.
Two independent sign_proposition calls successfully executed

Now we just have to bake a new block, to include our calls.

Going back to TzScan to verify our calls were included:

Two new transactions are visible, one for each sign_proposition call
Two signatures have been added to our contract’s storage

Executing the proposed transaction

This is it, the time has finally come. We’re going to execute a transaction, that has been signed by both multi-sig contract’s owners. We’ll use bootstrap1 to seal the deal, and ask the contract to execute_proposition.

Executing the proposed transaction
Successful call to execute_proposition

Once again, bake a new block to include our operations.

Going back to TzScan, we will notice that the contract’s storage is back at it’s original state, with nothing but owners defined, and 0.01tz has been transfered from out contract to bootstrap4.

Contract storage has been reset
0.01tz has been transferred to tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv → bootstrap4
Bootstrap4 has indeed received our multi-sig transaction

🎉 That’s it! We’ve successfully deployed a multi-sig smart contract, and we’ve been able to use it to propose a transaction, sign it and then execute it, resulting into balance updates in our contract and the target account.🎉

Let me know what you think

Do you have any questions or comments? Trouble running the code? Have you spotted a mistake?

I’d love to hear your thoughts, you can reach out to me at t.me/maht0rz or matej.sima@gmail.com

You can donate at:
ETH: 0x56ba1a8681DB66DD7F3158A5e2623577D2fE7ec2.
BTC: bc1qcc7j72wl7l8muepr3rewjfn3hdzq7h9j4rpr50
XTZ: tz1NjgqAjNLK6aXrpj78fdwTjc68Y9CLCrvf

Edit: thanks to Arthur Breitman for pointing out possible vulnerabilities in the contract, you can see the revision history here.

Additional resources

Glossary: https://zeronet.tzscan.io/glossary

ReasonML docs: https://reasonml.github.io/docs/en/what-and-why

TQGroup’s learn: https://learn.tqgroup.io/

Liquidity reference: http://www.liquidity-lang.org/doc/reference/liquidity.html

More by Matej Šima

Topics of interest

More Related Stories