ARC allowing coordination of multisig signing

Here is a discussion about creating a convention for wallets to handle multisig signing.

Multisignature accounts logically represent an ordered set of addresses with a threshold and version. Multisignature accounts can perform the same operations as other accounts, including sending transactions and participating in consensus. The address for a multi-signature account is essentially a hash of the ordered list of accounts, the threshold, and version values. The threshold determines how many signatures are required to process any transaction from this multi-signature account.

Example:

  1. Alice, Bob & Charlie have a multisig Account
  2. The threshold is 2/3
  3. A transaction needs to be signed

Alice wants to sign it with a browser wallet (Wallet-1), and Bob wants to use his phone(Wallet-2). Charlie is not there.
They are not using the same wallet but should be able to sign the transaction from where they want.

Alice signs the transaction with Wallet-1, Bob signs the transaction with Wallet-2.
Due to the threshold of 2/3, Wallet-2 should send the transaction.

Now that almost every wallet support ARC-1, we need a standard for this.

Feel free to share your idea.

2 Likes

Algo Builder team is building a multisig wallet (SigmaWallet), which will be responsible for:

  • creating multisig transactions,
  • signing using any web wallet provider (note: SigmaWallet doesn’t custody any keys, it delegates signing to other wallets)
  • broadcasting transactions (can be used when a final signature is done using kmd for example).

We are 70% complete, now ,mostly waiting for the wallets to provide missing features related to ARC-1 (Perra and myalgo). Algo Signer works perfectly.

www.a-wallet.net has multisig feature for about a year… btw it supports also rekeying of multisig accounts and much more :wink: And it is open source

it doesn’t support wallets, neither smart contract transactions.

Hey,

I have been working on something recently -with the introduction of boxes- that may well help with this. I’ve called it “Msig Signer” (please think of a better name), and it provides an on-chain solution for signing transactions by multisig signers. I was mostly trying to eliminate the need for sending transaction files around, and with boxes this can now be done on-chain and users only need to know the AppID of the smart contract.

Using an interface which knows how to extract the transaction data and also use the ABI to submit the signatures back to the app, it becomes trivial for individual signers to review and provide their signatures for a single or atomic group transaction, which has been created and set by the deployer of the smart contract.

One fun result of doing it like this is that once the signature threshold has been met, anyone can submit the transaction, so if someone keeps their window open with “Auto-Submit” ticked, it will be sent as soon as possible (assuming it’s within the first/last valid round range). As for downsides, obviously any transactions which will be signed this way will be entirely transparent to the public, so if you’re a private investment company, maybe you don’t want everyone seeing you’re about to make a large trade on a DEX.

Whilst the concept is fully working in my tests, I haven’t published the repo yet because no wallet currently supports boxes, so the UI still requires the user to run a command line method call to add/remove transactions.

Nevertheless I thought it would be good to share the details of the ABI and approval TEAL (below the screenshots) I have created so far and get feedback from others. Finally I have also created a web UI to demonstrate all this working (minus any box calls) which extracts the transactions from the boxes and presents them to the user to sign, then adds their signatures to the app.

Admin View:

Signer View:

{
  "name": "On-Chain Msig Signer",
  "networks": {},
  "methods": [
    {
      "name": "deploy",
      "desc": "Deploy a new On-Chain Msig Signer Application",
      "args": [
        { "type": "uint8", "name": "VersionMajor", "desc": "Major version number" },
        { "type": "uint8", "name": "VersionMinor", "desc": "Minor version number" }
      ],
      "returns": { "type": "uint64", "desc": "Msig Signer Application ID" }
    },
    {
      "name": "setSignatures",
      "desc": "Set signatures for account. Signatures must be included as an array of byte-arrays",
      "args": [
        { "type": "byte[][]", "name": "Signatures", "desc": "Array of signatures" }
      ],
      "returns": { "type": "void" }
    },
    {
      "name": "clearSignatures",
      "desc": "Clear signatures for account. Be aware this only removes it from your local state, and indexers will still know and could use your signatures",
      "args": [],
      "returns": { "type": "void" }
    },
    {
      "name": "addAccount",
      "desc": "Add account to multisig",
      "args": [
        { "type": "uint8", "name": "Account Index", "desc": "Account position within multisig" },
        { "type": "account", "name": "Account", "desc": "Account to add" }
      ],
      "returns": { "type": "void" }
    },
    {
      "name": "removeAccount",
      "desc": "Remove account from multisig",
      "args": [
        { "type": "uint8", "name": "Account Index", "desc": "Account position within multisig to remove" }
      ],
      "returns": { "type": "void" }
    },
    {
      "name": "addTransaction",
      "desc": "Add transaction to the app. Only one transaction should be included per call",
      "args": [
        { "type": "uint8", "name": "Group Index", "desc": "Transactions position within an atomic group" },
        { "type": "byte[]", "name": "Transaction", "desc": "Transaction to add" }
      ],
      "returns": { "type": "void" }
    },
    {
      "name": "removeTransaction",
      "desc": "Remove transaction from the app. Unlike signatures which will remove all previous signatures when a new one is added, you must clear all previously transactions if you want to reuse the same app",
      "args": [
        { "type": "uint8", "name": "Group Index", "desc": "Transactions position within an atomic group" }
      ],
      "returns": { "type": "void" }
    },
    {
      "name": "setThreshold",
      "desc": "Update the multisig threshold",
      "args": [
        { "type": "uint8", "name": "Threshold", "desc": "New multisignature threshold" }
      ],
      "returns": { "type": "void" }
    },
    {
      "name": "destroy",
      "desc": "Destroy the application and return funds to creator address. All transactions must be removed before calling destroy",
      "args": [],
      "returns": { "type": "void" }
    },
    {
      "name": "update",
      "desc": "Update the application",
      "args": [
        { "type": "uint8", "name": "VersionMajor", "desc": "New major version number" },
        { "type": "uint8", "name": "VersionMinor", "desc": "New minor version number" }
      ],
      "returns": { "type": "void" }
    }
  ]
}
#pragma version 8

// Deploy MsigTxnShare
// Args:
//  + uint8: Msig Signer Major Version
//  + uint8: Msig Signer Minor Version
// Returns:
//  + uint64: Application ID
method "deploy(uint8,uint8)uint64"
txna ApplicationArgs 0
==
bnz method_deploy

// Set Signatures
// Signatures can only be added by the account holder.
// Args:
//  + byte[][]: bytearray of bytearray containing 1 to 16 signatures
method "setSignatures(byte[][])void"
txna ApplicationArgs 0
==
bnz method_set_signatures

// Clear Signatures
method "clearSignatures()void"
txna ApplicationArgs 0
==
bnz method_clear_signatures

// Add Account
// Args:
//  + uint8: Multisig account index
//  + account: Account to add
method "addAccount(uint8,account)void"
txna ApplicationArgs 0
==
bnz method_add_account

// Remove Account By Index
// Args:
//  + uint8: Multisig account index
method "removeAccount(uint8)void"
txna ApplicationArgs 0
==
bnz method_remove_account

// Add Transaction
// Args:
//  + uint8: Group transaction index
//  + byte[]: Transaction bytes
method "addTransaction(uint8,byte[])void"
txna ApplicationArgs 0
==
bnz method_add_transaction

// Remove Transaction
// Args:
//  + uint8: Group transaction index
method "removeTransaction(uint8)void"
txna ApplicationArgs 0
==
bnz method_remove_transaction

// Set Threshold
// Args:
//  + New threshold
method "setThreshold(uint8)void"
txna ApplicationArgs 0
==
bnz method_set_threshold

// Destroy
method "destroy()void"
txna ApplicationArgs 0
==
bnz method_destroy

// Update
method "update(uint8,uint8)void"
txna ApplicationArgs 0
==
bnz method_update

err

method_deploy:
  txn OnCompletion
  int NoOp
  ==
  assert

  txn ApplicationID
  !
  assert

  // Store Msig Signer Major Version
  byte "VersionMajor"
  txna ApplicationArgs 1
  btoi
  app_global_put

  // Store Msig Signer Minor Version
  byte "VersionMinor"
  txna ApplicationArgs 2
  btoi
  app_global_put

  // Set threshold to 1 (default)
  byte "Threshold"
  int 1
  app_global_put

  // Return Application ID
  byte 0x151f7c75
  global CurrentApplicationID
  itob
  concat
  log

  int 1
  return

method_destroy:
  txn OnCompletion
  int DeleteApplication
  ==
  assert

  txn ApplicationID
  assert

  txn Sender
  global CreatorAddress
  ==
  assert

  itxn_begin

  int pay
  itxn_field TypeEnum

  global CreatorAddress
  itxn_field Receiver

  global CreatorAddress
  itxn_field CloseRemainderTo

  itxn_submit

  int 1
  return

method_update:
  txn OnCompletion
  int UpdateApplication
  ==
  assert

  txn ApplicationID
  assert

  txn Sender
  global CreatorAddress
  ==
  assert

  // Store Msig Signer Major Version
  byte "VersionMajor"
  txna ApplicationArgs 1
  btoi
  app_global_put

  // Store Msig Signer Minor Version
  byte "VersionMinor"
  txna ApplicationArgs 2
  btoi
  app_global_put

  int 1
  return

method_set_threshold:
  txn OnCompletion
  int NoOp
  ==
  assert

  txn ApplicationID
  assert

  txn Sender
  global CreatorAddress
  ==
  assert

  // Set threshold
  byte "Threshold"
  txna ApplicationArgs 1
  btoi
  app_global_put

  int 1
  return

method_add_transaction:
  txn OnCompletion
  int NoOp
  ==
  assert

  txn ApplicationID
  assert

  txn Sender
  global CreatorAddress
  ==
  assert

  // Delete the box if it already exists
  txna ApplicationArgs 1
  callsub sub_index_to_box
  box_del
  pop

  // Create the box
  txna ApplicationArgs 1    // 0
  callsub sub_index_to_box  // txn0
  txna ApplicationArgs 2    // txn0 abidata
  int 0                     // txn0 abidata 0
  extract_uint16            // txn0 txnlen
  box_create                // 1
  assert                    //

  // Place the txn in the box
  txna ApplicationArgs 1    // 0
  callsub sub_index_to_box  // txn0
  txna ApplicationArgs 2    // txn0 abidata
  dup                       // txn0 abidata abidata
  int 0                     // txn0 abidata abidata 0
  extract_uint16            // txn0 abidata txnlen
  int 2                     // txn0 abidata txnlen 2
  swap                      // txn0 abidata 2 txnlen
  int 2
  +
  substring3                // txn0 txndata
  box_put                   //

  int 1
  return

method_remove_transaction:
  txn OnCompletion
  int NoOp
  ==
  assert

  txn ApplicationID
  assert

  txn Sender
  global CreatorAddress
  ==
  assert

  // Delete the box if it exists
  txna ApplicationArgs 1
  callsub sub_index_to_box
  box_del
  pop

  int 1
  return

method_add_account:
  txn OnCompletion
  int NoOp
  ==
  assert

  txn ApplicationID
  assert

  txn Sender
  global CreatorAddress
  ==
  assert

  // If index already exists, remove index (and dec. account)
  int 0
  txna ApplicationArgs 1
  app_global_get_ex
  swap
  pop
  bz index_available
  txna ApplicationArgs 1
  callsub sub_remove_account_by_index

  index_available:
  // Store multisig index as key with account as value
  txna ApplicationArgs 1
  txna ApplicationArgs 2
  btoi
  txnas Accounts
  app_global_put

  // Store account as key and counter as value, this is
  // for ease of authentication, and tracking removal
  txna ApplicationArgs 2
  btoi
  txnas Accounts
  dup
  app_global_get
  int 1
  +
  app_global_put

  int 1
  return

method_remove_account:
  txn OnCompletion
  int NoOp
  ==
  assert

  txn ApplicationID
  assert

  txn Sender
  global CreatorAddress
  ==
  assert

  // Delete account by multisig index
  txna ApplicationArgs 1
  callsub sub_remove_account_by_index

  int 1
  return

// Scratch Slots:
//  0: Counter
//  1: Number of byte[] in byte[][]
//  2: Start of current byte[]
//  3: Size of current byte[]
method_set_signatures:
  txn OnCompletion
  int NoOp
  ==
  txn OnCompletion
  int OptIn
  ==
  ||
  assert

  txn ApplicationID
  assert

  callsub sub_is_sender_in_multisig
  assert

  // process byte[][]
  txna ApplicationArgs 1
  int 0
  extract_uint16
  store 1 // Number of arrays

  // Starting at index 0, add each sig to an indexed kv-pair
  int 0
  loop_add_sig:
  store 0
  txn Sender
  load 0
  itob
  substring 7 8

  // Get start of current byte[]
  // index * 2 + 2
  txna ApplicationArgs 1
  int 2
  int 2
  load 0
  *
  +
  extract_uint16
  store 2 // Start of byte[]

  // Get size of current byte[]
  txna ApplicationArgs 1
  int 2
  load 2
  +
  extract_uint16
  store 3 // Size of byte[]

  // Get signature from byte[]
  txna ApplicationArgs 1
  load 2 // Start of byte[]
  int 4 // 2 + 2 (size of byte[][] and size of current byte[])
  +
  dup
  load 3 // Size of byte[]
  +
  substring3
  app_local_put

  // Increment counter
  load 0
  int 1
  +
  dup

  // Check for additional signatures
  load 1
  <
  bnz loop_add_sig

  // Clear remaining signatures
  txn Sender
  swap
  callsub sub_clear_signatures

  int 1
  return

method_clear_signatures:
  txn OnCompletion
  int NoOp
  ==
  txn OnCompletion
  int CloseOut
  ==
  ||
  assert

  txn ApplicationID
  assert

  callsub sub_is_sender_in_multisig
  assert

  // Delete all 16 potential signatures
  txn Sender
  int 0
  callsub sub_clear_signatures

  int 1
  return

// Args:
//  + byte value (0x00 - 0x0f)
// Returns:
//  + byte value representing numbers (0 - 15)
sub_index_to_box:
  proto 1 1
  byte "txn"

  // Convert to uint
  frame_dig -1
  btoi
  frame_bury -1

  // Only allow values 0x00 to 0x0f (0-15)
  frame_dig -1
  int 16
  <
  assert

  // If larger than 9 we need to produce two digits
  frame_dig -1
  int 9
  >
  bnz sub_index_to_box_two_digits

  // Return single digit
  frame_dig 0
  frame_dig -1
  int 48
  +
  itob
  substring 7 8
  concat
  b end_sub_index_to_box

  sub_index_to_box_two_digits:
  frame_dig 0
  byte "1"
  frame_dig -1
  int 10
  %
  int 48
  +
  itob
  substring 7 8
  concat
  concat

  end_sub_index_to_box:
  frame_bury 0
  retsub

// Sender must have a keyed index in the multisig
sub_is_sender_in_multisig:
  proto 0 1
  int 0
  global CurrentApplicationID
  txn Sender
  app_global_get_ex
  swap
  pop
  frame_bury 0
  retsub

// Clear signatures from Ith position to 16th position
// Args:
//  + Account
//  + uint64: Ith position
sub_clear_signatures:
  proto 2 0

  // Delete remaining signatures
  loop_del_sig:
  frame_dig -2
  frame_dig -1
  itob
  substring 7 8
  app_local_del

  // Increment counter
  frame_dig -1
  int 1
  +
  dup
  frame_bury -1
  int 16
  <
  bnz loop_del_sig

  retsub

// Remove index from multisig
// Args:
//  + uint8: Ith position
sub_remove_account_by_index:
  proto 1 0

  // Fetch account before deleting
  frame_dig -1            // arg1
  app_global_get          // addr

  // Delete account by multisig index
  frame_dig -1            // addr arg1
  app_global_del          // addr

  // Decrement account counter, or delete if no longer used
  dup                     // addr addr
  app_global_get          // addr counter
  int 1                   // addr counter 1
  -                       // addr counter-1
  dup                     // addr counter-1 counter-1
  bnz keep_addr           // addr counter-1
  pop                     // addr
  app_global_del          //
  b end_remove_account
  keep_addr:              // addr counter-1
  app_global_put          //

  end_remove_account:
  retsub
4 Likes

Based off of @nullun’s idea, I have built a working model into AlgoTools. It would be great if the standard for the contract was enshrined in an ARC.

1 Like

@StephaneBarroso any movement on this or feedback on my implementation? Thanks.

@nullun we started to work on one. Yes, it has not been published yet.
From what I have tested, your implementation is on point. I shared your project every time someone asked about a multisig solution.

1 Like