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