Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
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:
- 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.
Account aliased as bootstrap1 and it’s keysAccount 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:
- 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.
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:
- 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 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.
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.
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 timeBaking 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 correctlyThere are no transactions yet, and the balance is 1000tzCreating 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 bootstrap4 →tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv 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.
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.
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 includedStorage has been updated with the proposed transactionSigning 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 callTwo signatures have been added to our contract’s storageExecuting 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.
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 reset0.01tz has been transferred to tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv → bootstrap4Bootstrap4 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
Implementing a Multi-Sig Smart-Contract in Tezos; using ReasonML was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.