Multisig Wrapper Quick Start

Overview

While there is a Generic Multisig Contract that allows a quorum of signers to approve arbitrary Michelson commands on the blockchain, it behaves more like a user whose arbitrary action is voted upon than a gatekeeper.

What is meant by "arbitrary action"? That generic multisig contract accepts arbitrary Michelson code as its main input: signers need to know what this code is doing to know what they're voting on.

We use another approach: the multisig contract is specialized to the particular contract you'd like to use it with and will only perform:

  • The exact actions your contract expects
  • Two actions to maintain the multisig functionality.

This prevents any action that the base contract does not support from being executed.

There are four main steps to setting up and interacting with a wrapped multisig contract:

  1. Use lorentz-contract with lorentz-contract-storage to originate your contract with multisig functionality
  2. Make a parameter file for an action you want to perform
  3. Sign the file with a quorum of users
  4. Submit the file to the multisig-wrapped contract

Setting up

Follow the Client Setup guide to set up:

  • The Tezos client
  • lorentz-contract-param
  • A test network wallet

Don't forget to set ALICE_ADDRESS to the address of the test network wallet!

ALICE_ADDRESS="tz1L2UAwwU4k2nxjzB2mTnxW61wCcWaZeYkp"

Getting your public key

We'll want our public/private keys to work with the client:

Here's a convenient way to get them, assuming tezos-client has registered/activated your account:

get_public_key(){ tezos-client show address $1 2>/dev/null | tail -n 1 | cut -d " " -f 3;}
get_secret_key(){ tezos-client show address $1 -S 2>/dev/null | tail -n 1 | cut -d ":" -f 3;}
$ get_public_key alice
edpktkQJBwKE8kVMgppcMkBtThaRx4uJm37qcuEKAL4hC4Hn579YDW

Originating a Lorentz contract

An example contract is a simple natural-number storage contract, which is registered as WrappedMultisigContractNat.

To see a list of supported contracts and actions, run: lorentz-contract-param --help

We're going to use:

  • Initial value: 7
  • Threshold: 1
    • The minimum number of signers is one
  • Signer keys: "[$(get_public_key alice)]"
    • alice is the only signer
$ tezos-client --wait none originate contract WrappedMultisigContractNat \
transferring 0 from $ALICE_ADDRESS running \
"$(lorentz-contract print --name WrappedMultisigContractNat --oneline)" \
--init "$(lorentz-contract-storage WrappedMultisigContractNat \
--initialNat 7 --threshold 1 --signerKeys "[$(get_public_key alice)]")" \
--burn-cap 1.432
Waiting for the node to be bootstrapped before injection...
Current head: BM6MSeM9AmAF (timestamp: 2019-10-22T19:09:04-00:00, validation: 2019-10-22T19:09:41-00:00)
Node is bootstrapped, ready for injecting operations.
Estimated gas: 40891 units (will add 100 for safety)
Estimated storage: 1423 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'oofrMFn21tJNVejEtAP1Hif29QSb4NAf1QH7ggPmLmGy2yLfFnx'
NOT waiting for the operation to be included.
Use command
tezos-client wait for oofrMFn21tJNVejEtAP1Hif29QSb4NAf1QH7ggPmLmGy2yLfFnx to be included --confirmations 30 --branch BLhXQp8zN61iykmseDCmu6MS9YBhSuMhGXnFmygPSYuakQvnf3y
and/or an external block explorer to make sure that it has been included.
This sequence of operations was run:
Manager signed operations:
From: tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ
Fee to the baker:0.005486
Expected counter: 2059
Gas limit: 40991
Storage limit: 1443 bytes
Balance updates:
tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ ............ -0.005486
fees(tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU,24) ... +0.005486
Origination:
From: tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ
Credit:0
Script:
{...}
Initial storage:
(Pair {}
(Pair (Pair { DUP ;
CAR ;
DIP { CDR } ;
SWAP ;
DUP ;
CAR ;
DIP { CDR } ;
DIP { SWAP ; PAIR ; CAR ; NIL operation ; PAIR ; DUP ; CAR ; DIP { CDR } } ;
SWAP ;
DIP { PAIR } ;
PAIR }
7)
(Pair 0 (Pair 1 { "edpkvYi2fWcWrDrx36ux2F8z39Yrctrn3vZpiRG13zL45ExxGoPMoP" }))))
No delegate for this contract
This origination was successfully applied
Originated contracts:
KT1QVwPWxezxF7PCcLjbLuq5qBYLwb48VoSZ
Storage size: 1166 bytes
Updated big_maps:
New map(58) of type (big_map bool unit)
Paid storage size diff: 1166 bytes
Consumed gas: 40891
Balance updates:
tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ ... -1.166
tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ ... -0.257
New contract KT1QVwPWxezxF7PCcLjbLuq5qBYLwb48VoSZ originated.
Contract memorized as WrappedMultisigContractNat.

Once the operation is included and we find the contract address, we can make a convenient variable for it:

ONESIG_NAT="KT1QVwPWxezxF7PCcLjbLuq5qBYLwb48VoSZ"

We can then ensure that the storage was initialized correctly:

$ tezos-client get contract storage for $ONESIG_NAT
Pair {}
(Pair (Pair { DUP ;
CAR ;
DIP { CDR } ;
SWAP ;
DUP ;
CAR ;
DIP { CDR } ;
DIP { SWAP ; PAIR ; CAR ; NIL operation ; PAIR ; DUP ; CAR ; DIP { CDR } } ;
SWAP ;
DIP { PAIR } ;
PAIR }
7)
(Pair 0 (Pair 1 { "edpktkQJBwKE8kVMgppcMkBtThaRx4uJm37qcuEKAL4hC4Hn579YDW" })))

Explanation:

  • On the first line:
    • The {} represents any big map: big maps always display as {}, regardless of their contents
  • On the second-to-last-line:
    • The 7 is our initial value
  • On the last line:
    • The 0 is the current counter (to prevent replay attacks)
    • The 1 is the threshold
    • The { "edpktkQJBwKE8kVMgppcMkBtThaRx4uJm37qcuEKAL4hC4Hn579YDW" } is the list of signer public keys

Performing an action on a Lorentz contract

While WrappedMultisigContractNat only has a single action, viz. to update its storage variable, and we're only using a single signer, the core steps are essentially the same for any Lorentz contract.

To facilitate the coordination of multiple signers, lorentz-contract-param generates a file for the signers to sign.

Each signer may sign a copy of the file, add their signature to an already-signed file, or omit signing the file. (It will be rejected without a quorum.)

Generating a multisig parameter file

You can see all available actions to sign using:

lorentz-contract-param --help

If the contract is multisig, a file will be generated to sign, otherwise the parameters will be output directly.

We can see the expected parameters using lorentz-contract-param [ACTION] --help:

$ lorentz-contract-param WrappedMultisigContractNat-new-nat --help
Usage: lorentz-contract-param WrappedMultisigContractNat-new-nat --counter NATURAL
--nat NATURAL
--signerKeys List PublicKey
WrappedMultisigContractNat parameter: new-nat
Available options:
-h,--help Show this help text
--counter NATURAL Natural number representing counter.
--nat NATURAL Natural number representing nat.
--signerKeys List PublicKey
Public keys of multisig signers.

We need:

  • The current counter value: 0
  • The new natural number we want to store: 42
  • The list of public keys for the contract: "[$(get_public_key alice)]"
$ lorentz-contract-param WrappedMultisigContractNat-new-nat \
--counter 0 --nat 42 --contractAddress $ONESIG_NAT \
--signerKeys "[$(get_public_key alice)]"
Writing parameter to file: "WrappedMultisigContractNat_0_26YbNeb2mnTmufswhPKLAiWSAmaWeb8kJXMntCd7cwxtEkSNgJ.json"

We can make a variable for the filename:

MULTISIG_PARAMETER_FILE="WrappedMultisigContractNat_0_26YbNeb2mnTmufswhPKLAiWSAmaWeb8kJXMntCd7cwxtEkSNgJ.json"

Signing a multisig parameter file

We use the action MultisigSignFile to sign our MULTISIG_PARAMETER_FILE:

$ lorentz-contract-param MultisigSignFile --help
Usage: lorentz-contract-param MultisigSignFile --secretKey SecretKey
--signerFile FilePath
Sign a contract parameter given a multisig signers file in JSON and a private
key
Available options:
-h,--help Show this help text
--secretKey SecretKey Private key to sign multisig parameter JSON file
--signerFile FilePath File path to multisig parameter JSON file

Important:

  • You should never provide a secret key directly on the command line, because it may be visible in your command history. Instead, use command substitution (as shown below) or a bash environment variable.
$ lorentz-contract-param MultisigSignFile \
--secretKey "$(get_secret_key alice)" --signerFile $MULTISIG_PARAMETER_FILE
Writing parameter to file: "WrappedMultisigContractNat_0_26YbNeb2mnTmufswhPKLAiWSAmaWeb8kJXMntCd7cwxtEkSNgJ.json"

Worried about repeating a signature? Here's what will happen:

$ lorentz-contract-param MultisigSignFile \
--secretKey "$(get_secret_key alice)" --signerFile $MULTISIG_PARAMETER_FILE
File has already been signed with the given key

These steps may be repeated, in any order, for any number of signers. In particular, the file may be copied, signed independently, and then the copies may be submitted all at once.

Once we have enough signatures, we can proceed to the next step to submit the action to the contract.

Submitting a multisig parameter file

Once a quorum of signers have signed the parameter file, we can convert the file to Michelson using lorentz-contract-param and send the resulting parameters to the contract:

$ tezos-client --wait none transfer 0 from $ALICE_ADDRESS to $ONESIG_NAT \
--arg "$(lorentz-contract-param MultisigSignersFile \
--signerFiles $MULTISIG_PARAMETER_FILE)"
Waiting for the node to be bootstrapped before injection...
Current head: BM1BDqDN5nw1 (timestamp: 2019-10-22T19:31:36-00:00, validation: 2019-10-22T19:32:02-00:00)
Node is bootstrapped, ready for injecting operations.
Estimated gas: 39020 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'onxBedxcN3USy7fojCQkpcbq6uXvnE6CAsSkuSmZQ5Zec6ejtGg'
NOT waiting for the operation to be included.
Use command
tezos-client wait for onxBedxcN3USy7fojCQkpcbq6uXvnE6CAsSkuSmZQ5Zec6ejtGg to be included --confirmations 30 --branch BM1BDqDN5nw18jCkKfWir9qFdCDBfdAK8AsLrizR65FzrgkCu1W
and/or an external block explorer to make sure that it has been included.
This sequence of operations was run:
Manager signed operations:
From: tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ
Fee to the baker:0.00429
Expected counter: 2062
Gas limit: 39120
Storage limit: 0 bytes
Balance updates:
tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ ............ -0.00429
fees(tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU,24) ... +0.00429
Transaction:
Amount:0
From: tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ
To: KT1Bo2wrJhAV7fCTL48G7QYifoEDMQ7HwDRT
Parameter: (Right
(Pair (Pair 0 (Left 42))
{ Some "edsigu6V3AuKYg7YTGqQxz33ky7gdrhs2mN98p4cyzttaek4gwG4Dqzkd1VArABry4dK2Nzt7tQ3kVXJWTYrXddH8P72sJ1Run6" }))
This transaction was successfully applied
Updated storage:
(Pair 59
(Pair (Pair { DUP ;
CAR ;
DIP { CDR } ;
SWAP ;
DUP ;
CAR ;
DIP { CDR } ;
DIP { SWAP ; PAIR ; CAR ; NIL operation ; PAIR ; DUP ; CAR ; DIP { CDR } } ;
SWAP ;
DIP { PAIR } ;
PAIR }
42)
(Pair 1
(Pair 1
{ 0x00fade454dd8cc6cddd147f90f66410206771a0f177e4ea8c6507a8c28aaeac84e }))))
Storage size: 1166 bytes
Consumed gas: 39020

The client can confirm that the update has occurred.

Note that the value has been updated to 42 and the counter is now 1.

$ tezos-client get script storage for $ONESIG_NAT
Pair {}
(Pair (Pair { DUP ;
CAR ;
DIP { CDR } ;
SWAP ;
DUP ;
CAR ;
DIP { CDR } ;
DIP { SWAP ; PAIR ; CAR ; NIL operation ; PAIR ; DUP ; CAR ; DIP { CDR } } ;
SWAP ;
DIP { PAIR } ;
PAIR }
42)
(Pair 1 (Pair 1 { "edpktkQJBwKE8kVMgppcMkBtThaRx4uJm37qcuEKAL4hC4Hn579YDW" })))

Transferring tez to the contract

First, we check the balance of the contract:

$ tezos-client get balance for $ONESIG_NAT
0

Next, we can transfer 1 ꜩ to the contract as follows:

$ tezos-client --wait none transfer 1 from $ALICE_ADDRESS to $ONESIG_NAT \
--arg "$(lorentz-contract-param WrappedMultisigContractNat-default)"
Waiting for the node to be bootstrapped before injection...
Current head: BLfZio2iVw6x (timestamp: 2019-10-22T19:32:36-00:00, validation: 2019-10-22T19:33:07-00:00)
Node is bootstrapped, ready for injecting operations.
Estimated gas: 37930 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'op69L6xk8XVs1S6DRGSgEt337q5Vus499Mrn2dYrscJJQRMprra'
NOT waiting for the operation to be included.
Use command
tezos-client wait for op69L6xk8XVs1S6DRGSgEt337q5Vus499Mrn2dYrscJJQRMprra to be included --confirmations 30 --branch BLfZio2iVw6xAjCVppa1mFXwMCFLgZuZwM9Zo1Gp4MGEkEBG2wz
and/or an external block explorer to make sure that it has been included.
This sequence of operations was run:
Manager signed operations:
From: tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ
Fee to the baker:0.004064
Expected counter: 2063
Gas limit: 38030
Storage limit: 0 bytes
Balance updates:
tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ ............ -0.004064
fees(tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU,24) ... +0.004064
Transaction:
Amount:1
From: tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ
To: KT1Bo2wrJhAV7fCTL48G7QYifoEDMQ7HwDRT
Parameter: (Left Unit)
This transaction was successfully applied
Updated storage:
(Pair 59
(Pair (Pair { DUP ;
CAR ;
DIP { CDR } ;
SWAP ;
DUP ;
CAR ;
DIP { CDR } ;
DIP { SWAP ; PAIR ; CAR ; NIL operation ; PAIR ; DUP ; CAR ; DIP { CDR } } ;
SWAP ;
DIP { PAIR } ;
PAIR }
42)
(Pair 1
(Pair 1
{ 0x00fade454dd8cc6cddd147f90f66410206771a0f177e4ea8c6507a8c28aaeac84e }))))
Storage size: 1166 bytes
Consumed gas: 37930
Balance updates:
tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ ... -1
KT1Bo2wrJhAV7fCTL48G7QYifoEDMQ7HwDRT ... +1

Finally, we verify that the contract now holds 1 ꜩ:

$ tezos-client get balance for $ONESIG_NAT
1

While WrappedMultisigContractNat doesn't need any tez, we might want to transfer tokens to a contract so that it may:

  • Originate other contracts
  • Call other contracts in a way that costs tez
  • Transfer tez to other contracts/users

Updating the signer list and threshold

The multisig contract allows updating the signer list and threshold.

We begin by making a new parameter file with:

  • counter: 1
  • threshold: 0
    • Note: this allows a minimum of zero signers
  • signerKeys: "[$(get_public_key alice)]"
    • Alice is currently the only signer
  • newSignerKeys: "[$(get_public_key fred)]"
    • Fred will be the only signer
$ lorentz-contract-param WrappedMultisigContractNat-change-keys --counter 1 \
--threshold 0 --contractAddress $ONESIG_NAT \
--signerKeys "[$(get_public_key alice)]" \
--newSignerKeys "[$(get_public_key fred)]"
Writing parameter to file: "WrappedMultisigContractNat_1_7jomDycXDpkYkhpp1idSPU5djorNrrM5XbiNhTWt8JLEFUbY7.json"

As before, we set a variable for the filename:

$ MULTISIG_PARAMETER_FILE="WrappedMultisigContractNat_1_7jomDycXDpkYkhpp1idSPU5djorNrrM5XbiNhTWt8JLEFUbY7.json"

And sign the file with a quorum (in this case, 1):

$ lorentz-contract-param MultisigSignFile --secretKey "$(get_secret_key alice)" \
--signerFile $MULTISIG_PARAMETER_FILE
Writing parameter to file: "WrappedMultisigContractNat_1_7jomDycXDpkYkhpp1idSPU5djorNrrM5XbiNhTWt8JLEFUbY7.json"

Finally, we send the signed parameter file to the contract:

$ tezos-client --wait none transfer 0 from $ALICE_ADDRESS to $ONESIG_NAT \
--arg "$(lorentz-contract-param MultisigSignersFile \
--signerFiles $MULTISIG_PARAMETER_FILE)"
Waiting for the node to be bootstrapped before injection...
Current head: BLF1wv2c3bkB (timestamp: 2019-10-22T19:20:34-00:00, validation: 2019-10-22T19:20:57-00:00)
Node is bootstrapped, ready for injecting operations.
Estimated gas: 39020 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'oo6GDM22uYkWSehWMvu36qqJUjnM2Wj7yNRYeukBBYikMbT62No'
NOT waiting for the operation to be included.
Use command
tezos-client wait for oo6GDM22uYkWSehWMvu36qqJUjnM2Wj7yNRYeukBBYikMbT62No to be included --confirmations 30 --branch BLF1wv2c3bkBzc5wCH8QSeKEyVGitTicXudTZRYnpNohFLnYoac
and/or an external block explorer to make sure that it has been included.
This sequence of operations was run:
Manager signed operations:
From: tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ
Fee to the baker:0.00429
Expected counter: 2060
Gas limit: 39120
Storage limit: 0 bytes
Balance updates:
tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ ............ -0.00429
fees(tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU,24) ... +0.00429
Transaction:
Amount:0
From: tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ
To: KT1QVwPWxezxF7PCcLjbLuq5qBYLwb48VoSZ
Parameter: (Right
(Pair (Pair 0 (Left 42))
{ Some "edsigttPzJJBNSrsPFt2WSj8c61vBbpRGb7RtzGzjSYPx11ESXnaKfWpPEFA5r2HT5j3Yb3q3DxqDxZLvSFBPoRosDxSacZvtPd" }))
This transaction was successfully applied
Updated storage:
(Pair 58
(Pair (Pair { DUP ;
CAR ;
DIP { CDR } ;
SWAP ;
DUP ;
CAR ;
DIP { CDR } ;
DIP { SWAP ; PAIR ; CAR ; NIL operation ; PAIR ; DUP ; CAR ; DIP { CDR } } ;
SWAP ;
DIP { PAIR } ;
PAIR }
42)
(Pair 1
(Pair 1
{ 0x00fade454dd8cc6cddd147f90f66410206771a0f177e4ea8c6507a8c28aaeac84e }))))
Storage size: 1166 bytes
Consumed gas: 39020

We may verify that the signers list and threshold were updated:

$ tezos-client get script storage for $ONESIG_NAT
Pair {}
(Pair (Pair { DUP ;
CAR ;
DIP { CDR } ;
SWAP ;
DUP ;
CAR ;
DIP { CDR } ;
DIP { SWAP ; PAIR ; CAR ; NIL operation ; PAIR ; DUP ; CAR ; DIP { CDR } } ;
SWAP ;
DIP { PAIR } ;
PAIR }
42)
(Pair 2 (Pair 0 { "edpkvMCwX3MyDg92HckSwFVofR8hcZEjAqrhWJ8SGQgkGjgK1V1gPo" })))
$ get_public_key fred
edpkvMCwX3MyDg92HckSwFVofR8hcZEjAqrhWJ8SGQgkGjgK1V1gPo

Next steps

lorentz-contract-param is set up to work with FA1.2, a.k.a. ManagedLedger, and several other built-in contracts. If you want to wrap your own contract, another variant of this one, etc., see the guide to wrapping your own contract with multi-signature functionality