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 technologies work. We will use a sandboxed Tezos for development of our smart contract, but the same process applies to other networks such as Alphanet or Mainnet. Let’s get started! blockchain network 🚨 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 . In our case, we want two users, to have . divide responsibility over funds 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 . 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 up to who controls the account (contract) bootstrap1 bootstrap5. Account aliased as bootstrap1 and it’s keys Account aliased as bootstrap2 and it’s keys Our owners shall be , 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. bootstrap1 & bootstrap2 ⚠️ 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: , , , . We’ll walk through them as we progress in our tutorial. create_proposition sign_proposition delete_proposition execute_proposition 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 , via their storage, which is defined as a type, and . store data on the chain 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 and we can call it . All the contract related code below should end up in our contract file. src/contracts multisig.re For the sake of simplicity, let’s initialise our storage right away as well. We’ll use & to set the in our storage. And we’ll leave out the other storage attributes ‘blank’, so a variant and an empty . bootstrap1 bootstrap2 owners None Set ACL — Access Control Lock Our smart contract needs to We can do that by comparing the via a transaction, with that we’ve specified as initial storage. limit access to certain entry points. address of the person, who called our contract address of owners Let’s define a method that we can re-use, we’ll call it , and it will accept a single parameter, which is the current storage value. Additionally, we’ll implement a helper method that makes the current contract transaction fail — we’ll use it when evaluates to . can_call fail_with_wrong_ownership can_call false Proposing a multi-sig transaction To propose a new transaction within our multi-sig contract, we’ll use a method called , which takes two arguments, one is of type and the second one is of type . create_proposition proposition 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 entry point, hence we use . delete_proposition 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 , if it passes, we’ll add a signature based on the → which is the address of the account which called our entry point in a transaction — and that’s one of the contract owners. delete_proposition Current.sender() only adds values to the set if they are unique, that means if you call the 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. _Set.add_ _sign_proposition_ 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 transaction, with destination & amount taken from our proposed transaction. Account.transfer We end the entry point execution by returning a cleaned up storage thanks to 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. delete_proposition 🎉 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 — 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. refmt Now we’ve got a file called , 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 straight away multisig.liq .liq Forging a deployment operation We will now that we can sign. Before we do so, let’s examine what exactly will go down. forge a deployment operation We’re using Liquidity’s CLI, with our local sandboxed node as $NODE_URL Amount of will be consumed by our contract, when executing proposed transactions. 1000tz Specified fee of , serves as an incentive for the bakers to include our operation in a new block. 1tz Source is the address of an account that’s facilitating the deployment — in our case it’s , who also happens to be an owner of the multi-sig contract and a baker in our sandboxed environment. bootstrap1 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 to the command below. copy the real signature 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 in your browser. http://localhost:8000/contracts 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 account from our client, which is not used anywhere in our contract. bootstrap3 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 ‘s keys and our smart contract address to perform a contract entry point call. We’ll be proposing a transaction to → with amount of . bootstrap3 bootstrap4 tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv 0.01tz ⚠️ When calling we also have to specify the parameter, in this case our . Syntax used to specify the parameter value is Liquidity/Ocaml. create_proposition new_proposition 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 ‘s keys, we’re able to call the contract successfully and create a proposition. bootstrap1 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 in two separate calls with different owners to sign and seal our proposition. We’ll use and to sign our proposition. sign_proposition bootstrap1 bootstrap2 If you’re feeling adventurous, you can try calling using an account, that’s not specified as an owner. It should fail thanks to our ACL restrictions. 👉 sign_proposition 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 to seal the deal, and ask the contract to . bootstrap1 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 has been transfered from out contract to . 0.01tz 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 or matej.sima@gmail.com t.me/maht0rz : 0x56ba1a8681DB66DD7F3158A5e2623577D2fE7ec2. You can donate at: ETH : bc1qcc7j72wl7l8muepr3rewjfn3hdzq7h9j4rpr50 BTC 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