Multisig Wrapper with Other Contracts

Introduction

The Multisig Wrapper CLI not only supports Lorentz contracts, e.g. nat storage and FA1.2, but it may also be used to wrap arbitrary contracts.

In this guide, the base contract that we'll be wrapping is a simple nat storage contract, but the same steps may be used for any contract.

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, wrapped 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
  • An alphanet wallet

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

ALICE_ADDRESS="tz1L2UAwwU4k2nxjzB2mTnxW61wCcWaZeYkp"

We're using:

$ lorentz-contract-param --version
lorentz-contract-param-1.2.0.1.4

Remember that alpha-client calls can take some time to respond, usually a couple of seconds, but sometimes up to a couple minutes.

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 multisig-wrapped contract

The contract we'll be using is a simple natural-number storage contract, which is registered in lorentz-contract-param as NatStorageContract.

We can store its source code in a variable (since we'll be reusing it):

NAT_CONTRACT_CODE="$(lorentz-contract print --name NatStorageContract --oneline)"

Wrapping the contract with multisig functionality

We can wrap our contract using lorentz-contract' print command, by passing the contract code through STDIN:

$ echo $NAT_CONTRACT_CODE | lorentz-contract print --name WrappedMultisig --oneline
parameter (or unit (pair (pair nat (or nat (pair nat (list key)))) (list (option signature))));storage (pair (big_map bool unit) (pair (pair (lambda (pair nat (pair (big_map bool unit) nat)) (pair (list operation) (pair (big_map bool unit) nat))) nat) (pair nat (pair nat (list key)))));code { DUP;CAR;DIP { CDR };IF_LEFT { DROP;NIL operation;PAIR } { DIP { DUP;CAR;DIP { CDR };DIP { DUP;CAR;DIP { CDR } };PAIR };SWAP;DIP { PUSH mutez 0;AMOUNT;COMPARE;EQ;IF { } { PUSH string "Some tokens were sent to this contract outside of the default entry point.";FAILWITH };SWAP;DUP;DIP { SWAP };DIP { DUP;CAR;DIP { CDR };DUP;SELF;ADDRESS;PAIR;PACK;DIP { DUP;CAR;DIP { CDR };DIP { SWAP } };SWAP };DUP;CAR;DIP { CDR };DIP { SWAP };COMPARE;EQ;IF { } { PUSH string "Counters do not match.";FAILWITH };DIP { SWAP };DUP;CAR;DIP { CDR };DIP { PUSH nat 0;SWAP;ITER { DIP { SWAP };SWAP;IF_CONS { IF_NONE { SWAP;DROP } { SWAP;DIP { SWAP;DIP { DIP { DIP { DUP };SWAP } };DIP { DIP { DUP };SWAP };SWAP;DIP { CHECK_SIGNATURE };SWAP;IF { DROP } { PUSH (pair string unit) (Pair "InvalidSignature" Unit);FAILWITH };PUSH nat 1;ADD } } } { PUSH (pair string unit) (Pair "FewerSignaturesThanKeys" Unit);FAILWITH };SWAP } };COMPARE;LE;IF { } { PUSH string "Quorum not present";FAILWITH };IF_CONS { PUSH (pair string unit) (Pair "UncheckedSignaturesRemain" Unit);FAILWITH } { };DROP;DIP { DUP;CAR;DIP { CDR };PUSH nat 1;ADD;PAIR } };SWAP;DIP { SWAP };IF_LEFT { DIP { SWAP;DUP;CAR;DIP { CDR };DIP { DUP;CAR;DIP { CDR };SWAP;DIP { DUP } };PAIR };PAIR;EXEC;DUP;CAR;DIP { CDR };DIP { DUP;CAR;DIP { CDR };DIP { SWAP;PAIR };PAIR } } { DIP { CAR };SWAP;PAIR;SWAP;NIL operation };DIP { DUP;CAR;DIP { CDR };DIP { PAIR };PAIR };PAIR } };

Generating the initial storage value

To generate the initial storage value, we're using the WrappedMultisigContractGeneric action for lorentz-contract-storage:

$ lorentz-contract-storage WrappedMultisigContractGeneric --help
Usage: lorentz-contract-storage WrappedMultisigContractGeneric --contractName STRING
[--contractFilePath STRING]
--contractInitialStorage STRING
--threshold NATURAL
--signerKeys List PublicKey
Make initial storage for some wrapped Michelson contract. Omit the
'contractFilePath' option to pass the contract through STDIN.
Available options:
-h,--help Show this help text
--contractName STRING String representing the contract's initial
contractName.
--contractFilePath STRING
String representing the contract's initial
contractFilePath.
--contractInitialStorage STRING
String representing the contract's initial
contractInitialStorage.
--threshold NATURAL Natural number representing threshold.
--signerKeys List PublicKey
Public keys of multisig signers.

We're going to use:

  • Initial value: 7
    • Note that this is the raw Michelson value of the contract's initial storage
  • Threshold: 1
    • The minimum number of signers is one
  • Signer keys: "[$(get_public_key alice)]"
    • alice is the only signer
  • Contract: NAT_CONTRACT_CODE
    • Passed through STDIN, using echo $NAT_CONTRACT_CODE | lorentz-contract-storage ..
$ echo $NAT_CONTRACT_CODE | lorentz-contract-storage \
WrappedMultisigContractGeneric --contractName NatStorageContract --threshold 1 \
--signerKeys "[$(get_public_key alice)]" --contractInitialStorage 7
(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"}))))

Originating the contract

To originate the wrapped contract, we pass the wrapped contract code and generated initial storage to the client:

WrappedMultisigContractNat \ transferring 0 from $ALICE_ADDRESS
running \ "$(echo $NAT_CONTRACT_CODE | lorentz-contract print \ --name
WrappedMultisig --oneline)" \ --init "$(echo $NAT_CONTRACT_CODE |
lorentz-contract-storage \ WrappedMultisigContractGeneric
--contractName NatStorageContract --threshold 1 \ --signerKeys
"[$(get_public_key alice)]" --contractInitialStorage 7)" \ --burn-cap
1.432
Waiting for the node to be bootstrapped before injection... Current
head: BL3gNxrEZbsc (timestamp: 2019-10-22T19:42:36-00:00, validation:
2019-10-22T19:42:53-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
'oomwJ6Geauc6nKNMybCeyCKcq8mTdisqfPAxPiJwYB7yUcnV8ui' NOT waiting for
the operation to be included. Use command tezos-client wait for
oomwJ6Geauc6nKNMybCeyCKcq8mTdisqfPAxPiJwYB7yUcnV8ui to be included
--confirmations 30 --branch
BL3gNxrEZbscwHPa1qrVSnHMoKNXcpN7iyxAD25SGcQGquJU92p 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: 2064 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: KT1DdHNJxaCeB91oE4ASBXh7Ey7xEMgnJkZF Storage
size: 1166 bytes Updated big_maps: New map(60) 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 KT1DdHNJxaCeB91oE4ASBXh7Ey7xEMgnJkZF originated.
Contract memorized as WrappedMultisigContractNat3.

After waiting for the operation to be included, set the address of the originated contract:

NAT_CONTRACT_ADDRESS="KT1DdHNJxaCeB91oE4ASBXh7Ey7xEMgnJkZF"

We can ensure that the storage was initialized correctly:

$ alpha-client get script storage for $NAT_CONTRACT_ADDRESS
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

Interacting with a multisig-wrapped 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

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

Two actions may be performed on any multisig-wrapped contract without specifying a base contract, viz.:

  • WrappedMultisig-default: transfer tokens to the contract. Does not require a quorum of signatures.
  • WrappedMultisig-change-keys: change the signer keys and threshold. Does require a quorum of signatures.

These are featured in the multisig quick start guide and so this guide only covers MultisigSomeOperationParams, the only command which requires the base contract to be specified.

We can see the expected parameters using --help:

$ lorentz-contract-param MultisigSomeOperationParams --help
Usage: lorentz-contract-param MultisigSomeOperationParams --contractName STRING
[--contractFilePath FilePath]
--contractAddress ADDRESS
--counter NATURAL
--contractParam MICHELSON_VALUE
--signerKeys List PublicKey
Generate a multisig parameter file for an arbitrary multisig-wrapped contract.
Omit the 'contractFilePath' option to pass the contract through STDIN.
Available options:
-h,--help Show this help text
--contractName STRING Contract name
--contractFilePath FilePath
File path to the base contract source
--contractAddress ADDRESS
Address of the contractAddress.
--counter NATURAL Natural number representing counter.
--contractParam MICHELSON_VALUE
Contract parameter
--signerKeys List PublicKey
Public keys of multisig signerKeys.

We need:

  • The current counter value: 0
  • The new natural number we want to store: 42
    • Note that this is the raw Michelson value of the contract's parameter
  • The list of public keys for the contract: "[$(get_public_key alice)]"
$ echo $NAT_CONTRACT_CODE | lorentz-contract-param MultisigSomeOperationParams \
--contractName WrappedMultisigContractNat \
--contractAddress $NAT_CONTRACT_ADDRESS --counter 0 --contractParam 42 \
--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 and submitting a multisig parameter file

At this point, the signing and submission steps are the same as if we used a supported Lorentz contract, e.g. FA1.2.

We use the action MultisigSignFile to sign our MULTISIG_PARAMETER_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"

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:

$ alpha-client --wait none transfer 0 from $ALICE_ADDRESS to $NAT_CONTRACT_ADDRESS \
--arg "$(lorentz-contract-param MultisigSignersFile \
--signerFiles $MULTISIG_PARAMETER_FILE)"
Waiting for the node to be bootstrapped before injection...
Current head: BMYXXLBGpgRW (timestamp: 2019-10-22T19:45:26-00:00, validation: 2019-10-22T19:45:48-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 'ooi3f7NPrwTBrUVJEqQiVcF2ZcyAyHhfMcjbibqkvdCFavC5cSB'
NOT waiting for the operation to be included.
Use command
tezos-client wait for ooi3f7NPrwTBrUVJEqQiVcF2ZcyAyHhfMcjbibqkvdCFavC5cSB to be included --confirmations 30 --branch BMYXXLBGpgRWEwV3f8VqNf5u4A8M4PQY84K8ZXrWJz3YbsLa9iV
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: 2065
Gas limit: 39120
Storage limit: 0 bytes
Balance updates:
tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ ............ -0.00429
fees(tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU,24) ... +0.00429
Transaction:
Amount:0
From: tz1VhNvW3aWCHRNawF4sHfBfJAu5WrpcEbSQ
To: KT1DdHNJxaCeB91oE4ASBXh7Ey7xEMgnJkZF
Parameter: (Right
(Pair (Pair 0 (Left 42))
{ Some "edsigu52A5sSQ511AU4JWgMrbeJDMMWuCVquNGZ3MXUiAicvQo2kMFxX98kZuuR4hscsNsLUShtxAmXzBN9ShMH3vf64qsq6mZw" }))
This transaction was successfully applied
Updated storage:
(Pair 60
(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:

$ alpha-client get script storage for $NAT_CONTRACT_ADDRESS
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" })))