Multi-signature (multi-sig) wallets and accounts let multiple people access their assets based on specific parameters. A multi-sig wallet might contain a decentralized autonomous organization's (DAO) or other group's communal treasury, for example. The wallet might require 25 percent of the members to sign a transaction before any assets can be moved. 

As opposed to simple one-owner wallets, multi-sig wallets open up a world of possible uses, letting you create game components and commerce apps, as two examples, that can only be unlocked by multiple users. Requiring a quorum of users, or setting other unlock conditions, keeps digital assets secure from a single rogue user raiding an account.

A multi-sig smart contract, written in Move, serves as the primary building block for this type of app on Sui. The multi-sig smart contract presented in this article verifies multi-sig addresses, and supports different combinations of keys, such as 2-of-3 or M-of-N, where M and N are user-defined parameters.

Creating and using the multi-sig checker contract in Move

A multi-sig address is a special type of address that requires multiple signatures to authorize a transaction. A multi-sig checker smart contract derives a multi-sig address from a set of public keys, weights, and a threshold, and compares it with an expected address. 

Multi-sig addresses require multiple signatures to authorize a transaction. They are often used to enhance the security of funds by distributing control among multiple parties. For example, a 2-of-3 multi-sig address requires at least two out of three signers to approve a transaction. Multi-sig addresses can also be used for governance, escrow, or backup purposes.

The multi-sig smart contract performs three functions. It derives multi-sig addresses, verifies them, and can check a sender's multi-sig address.

Derive multi-sig addresses

The multisig module defines the

derive_multisig_address_quiet

which takes three parameters: pks, weights, and threshold. 

The

pks

parameter is a vector of vectors of bytes, representing the public keys of the signers.

The

weights

parameter is a vector of bytes, representing the weights of each signer.

The

threshold

parameter is a 16-bit unsigned integer, representing the minimum sum of weights required to execute a transaction from the multi-signature address.

The function returns an address, which is the derived multi-signature address.

public fun derive_multisig_address_quiet(         pks: vector<vector<u8>>,         weights: vector<u8>,         threshold: u16,     ): address {

 The function performs the following steps:

It defines a variable,

multiSigFlag

, of type 8-bit unsigned integer and assigns it the value 0x03, which is the flag for multi-signature addresses.

let multiSigFlag = 0x03;

It creates an empty vector of bytes called

hash_data

, which will store the data to be hashed.

let hash_data = vector<u8>[];

It gets the lengths of the

pks

and weights vectors and checks that they are equal. If not, it aborts the execution with an error code: ELengthsOfPksAndWeightsAreNotEqual.

let pks_len = pgs.length(); let weights_len = weights.length(); assert!(pks_len == weights_len, ELengthsOfPksAndWeightsAreNotEqual);

It initializes a variable,

sum

, of type 16-bit unsigned integer and assigns it the value 0. It then loops through the weights vector and adds the values of each element to the sum. It then checks that the threshold is positive and not greater than the sum. If not, it aborts the execution with an error code: EThresholdIsPositiveAndNotGreaterThanTheSumOfWeights.

       let mut sum = 0;         let mut i = 0;         while (i < weights_len) {             let w = weights[i] as u16;             sum = sum + w;             i = i + 1;         };         assert!(threshold > 0 && threshold <= sum, EThresholdIsPositiveAndNotGreaterThanTheSumOfWeights);

 It pushes the

multiSigFlag

to the hash_data vector. It then serializes the threshold using the bcs::to_bytes function and appends the result to the hash_data vector.

       hash_data.push_back(multiSigFlag);        let threshold_bytes: vector<u8> = bcs::to_bytes(&threshold);        hash_data.append(threshold_bytes);

It loops through the

pks

and weights vectors and appends the elements of each pair to the hash_data vector.

        let mut i = 0;         while (i < pks_len) { hash_data.append(pks[i]); hash_data.push_back(weights[i]);           i = i + 1;         };

It hashes the

hash_data

vector using the blake2b256 function and converts the result to an address using the address::from_bytes function. It then assigns the address to a variable, ms_address, and returns it.

        let ms_address = address::from_bytes(blake2b256(&hash_data));         ms_address     }

It derives a multi-sig address and returns the multi-sig address.

Verifying multi-sig addresses

The

multisig

module also defines the check_multisig_address_eq, which checks if the created multi-sig address matches the expected address. As we mentioned above, a multi-sig address is a special type of address that requires multiple signatures to authorize a transaction. A multi-sig address is defined by a set of public keys, weights, and a threshold.

The function

check_multisig_address_eq

takes four parameters: pks, weights, threshold, and expected_address. The first three parameters are the same as the ones we used in the previous function, derive_multisig_address_quiet. The last parameter, expected_address, is an address value that we want to compare with the multi-sig address.

   public entry fun check_multisig_address_eq(         pks: vector<vector<u8>>,         weights: vector<u8>,         threshold: u16,         expected_address: address,     ): bool {

The function first calls the function,

derive_multisig_address_quiet

, which creates a multi-sig address from the given public keys, weights, and threshold. This function uses a hash-based algorithm to combine the public keys and the threshold into a 16-byte value, which is then converted into an address.

let ms_address = derive_multisig_address_quiet(pks, weights, threshold);

 The function then compares the multi-sig address with the expected address and returns true if the addresses are equal, and false otherwise.

return (ms_address == expected_address)

The function

check_multisig_address_eq

can be used to verify that a multi-sig address is correct and matches the expected value. This can be useful for testing, debugging, or auditing purposes. For example, one could use this function to check that a multi-sig address is consistent with the public keys and the threshold that were agreed upon by the signers.

Checking the sender’s multi-sig address

Finally, the

multisig

module also defines the check_if_sender_is_multisig_address, which checks if the sender is the same multi-sig address that is derived from the provided pks, weights, and threshold.

The

check_if_sender_is_multisig_address

takes four parameters: pks, weights, threshold, and ctx. The first three parameters define the multi-sig address scheme, while the last parameter provides the transaction context.

The

pks

parameter is a vector of vectors of bytes, representing the public keys of the signers.

The

weights

parameter is a vector of bytes, representing the weights of each signer.

The

threshold

parameter is a 16-bit unsigned integer, representing the minimum sum of weights required to execute a transaction from the multi-sig address.

Finally, the

ctx

is a mutable reference to the TxContext, which contains information about the current transaction, such as the sender.

   public fun check_if_sender_is_multisig_address(         pks: vector<vector<u8>>,         weights: vector<u8>,         threshold: u16,         ctx: &mut TxContext     ): bool {

The

check_if_sender_is_multisig_address

function calls the check_multisig_address_eq function, which compares the multi-sig address with the sender address.

      check_multisig_address_eq(pks, weights, threshold, ctx.sender())             }

The function

check_multisig_address_eq

returns true if the sender address matches the multi-sig address scheme, and false otherwise.

Get started with multi-sig

Multi-sig addresses are useful for scenarios where there is a need for enhanced security, accountability, or collaboration among multiple parties. Given the valuable digital assets stored on Sui, a multi-sig address can help keep those assets secure.

The smart contract described in this article can help you get started building applications designed for collaboration and joint custody of assets. As a further resource, you can look at the source code and documentation for this project on GitHub.