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.
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!
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:
- Clone the repository
- Choose an environment
- Build the images
- Start the node, it takes roughly 30 seconds to start
- 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
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.
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
We’ll expose four entry points in our contract:
execute_proposition. We’ll walk through them as we progress in our tutorial.
Flow should go as the following:
- Propose a new transaction
- Sign the proposed transaction with all contract owners (individual entry point calls)
- 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.
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:
- Set of owners — basically ‘an array’ of addresses, that can operate our smart contract
- Proposition — A proposed transaction by one of the contract owners, denoting what amount and to whom it should be sent. (destination, amount)
- 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.
For the sake of simplicity, let’s initialise our storage right away as well. We’ll use
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
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
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
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
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.addonly adds values to the set if they are unique, that means if you call the
sign_propositionentry 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
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.
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
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
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
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.
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.
🎉 You can now check TzScan again, and you’ll see our multi-sig contract deployed 🎉
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 —
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.
Then we copy our deployed smart contract’s address from here:
In my case, the contract’s address is
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
tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv with amount of
⚠️ When calling
create_propositionwe also have to specify the parameter, in this case our
new_proposition. Syntax used to specify the parameter value is Liquidity/Ocaml.
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.
For our contract call to take effect, we must bake a new block in our sandboxed environment again:
tezos-client bake for bootstrap1.
We can verify that our call/transaction was included in the chain, using TzScan:
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
bootstrap2 to sign our proposition.
👉 If you’re feeling adventurous, you can try calling
sign_propositionusing an account, that’s not specified as an owner. It should fail thanks to our ACL restrictions.
Now we just have to bake a new block, to include our calls.
Going back to TzScan to verify our calls were included:
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
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
🎉 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 email@example.com
You can donate at:
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