Multisig Wallet
Introduction
Multisignature wallets are cryptocurrency wallets that require multiple private keys to sign and authorize a transaction.
To illustrate, think of a bank vault that demands multiple keys to unlock; this analogy captures how multisignature cryptocurrency wallets operate.
Advocates of multisignature wallets argue that they offer the most secure and fail-proof method for storing cryptocurrency. Even if a thief were to obtain one of the wallet keys, they would still be unable to access the account without the keys associated with the other wallets in the setup.
This article provides an explanation of the programming interface, data structure, basic functions, and their respective purposes. It can be used as-is or customized to fit specific needs. Anyone can create an application and deploy it on the Gear Network. The source code, developed using the Sails framework, is available on GitHub.
Logic
The wallet belongs to one or more owners. For any significant action to occur, the necessary number of owners must confirm it.
The contract deployer can determine the permitted number of owners who can initiate a transaction from the wallet and specify the minimum required owners for the transaction. For instance, they can opt for a 2-of-3 multisig, where two out of three assigned private keys are necessary. The flexibility extends to configurations like 3-of-5, 5-of-7, and so forth.
To send a transaction through the multisig wallet, one of the owners should send a transaction to the wallet with a SubmitTransaction action in the payload, and other owners should approve this transaction by the ConfirmTransaction action until the required amount is reached.
The transaction description allows the owner to include helpful information.
The wallet offers flexibility, enabling users to oversee the list of owners and determine the necessary confirmations. Security is a priority in the contract, restricting actions like adding, removing, or replacing owners and altering confirmation counts to only be executed with the requisite confirmations from other owners.
The transaction approval logic is complex, for example:
- When the owner submits a transaction requiring only one confirmation for execution, the contract adds it to the storage, obtains confirmation from the submitting owner, and then automatically executes the transaction.
- For transactions needing two or more confirmations, the contract follows a similar process. Initially, it adds the transaction to the storage and secures confirmation from the submitting owner. To execute the transaction, the wallet still requires one or more confirmations. Subsequently, another owner sends a
ConfirmTransactionaction to the contract. If all is well, the transaction is executed automatically.
Transactions usually happen automatically when all confirmations are done. However, if a transaction was confirmed
ntimes, and the contract needsn + 1or more confirmations, and then the owners change it tonor less, they can either wait for the next confirmation or just callExecuteTransactionwith the transaction ID to execute it.
Interface
Program description
The program contains the following information
pub struct MultisigWallet {
pub transactions: HashMap<TransactionId, Transaction>,
pub confirmations: HashMap<TransactionId, HashSet<ActorId>>,
pub owners: HashSet<ActorId>,
pub required: u32,
pub transaction_count: U256,
}transactions- a mapping ofTransactionIdtoTransactionthat stores all transactions in the walletconfirmations- a mapping ofTransactionIdto a set ofActorIds, representing confirmations by each owner for specific transactionsowners- a set ofActorIdsrepresenting the authorized owners of the walletrequired- the number of confirmations required to execute a transactiontransaction_count- a counter tracking the number of transactions
Where Transaction is defined as follows:
pub struct Transaction {
pub destination: ActorId,
pub payload: Vec<u8>,
pub value: u128,
pub description: Option<String>,
pub executed: bool,
}destination- the account where the transaction funds or actions are directedpayload- data related to the transaction, potentially containing specific instructions or actionsvalue- the amount of funds or value to transferdescription- an optional description providing context for the transactionexecuted- a boolean indicating if the transaction has been executed
Initialization
During program initialization, the following variables are set: the list of wallet owners and the minimum number of confirmations required to execute a transaction.
fn init(owners: Vec<ActorId>, required: u32) -> Self {
let owners_count = owners.len();
validate_requirement(owners_count, required);
let mut wallet = MultisigWallet::default();
for owner in &owners {
if wallet.owners.contains(owner) {
panic!("The same owner contained twice")
} else {
wallet.owners.insert(*owner);
}
}
wallet.required = required;
unsafe { STORAGE = Some(wallet) };
Self(())
}Actions
pub fn add_owner(&mut self, owner: ActorId);
pub fn remove_owner(&mut self, owner: ActorId);
pub fn replace_owner(&mut self, old_owner: ActorId, new_owner: ActorId);
pub fn change_required_confirmations_count(&mut self, count: u32);
pub fn submit_transaction(
&mut self,
destination: ActorId,
data: Vec<u8>,
value: u128,
description: Option<String>,
);
pub fn confirm_transaction(&mut self, transaction_id: U256);
pub fn revoke_confirmation(&mut self, transaction_id: U256);
pub fn execute_transaction(&mut self, transaction_id: U256);add_owneris an action to add a new owner. The action has to be used throughsubmit_transaction.remove_owneris an action to remove an owner. The action has to be used throughsubmit_transaction.replace_owneris an action to replace an owner with a new owner. The action has to be used throughsubmit_transaction.change_required_confirmations_countis an action to change the number of required confirmations. The action has to be used throughsubmit_transaction.submit_transactionis an action that allows an owner to submit and automatically confirm a transaction.confirm_transactionis an action that allows an owner to confirm a transaction. If this is the last confirmation, the transaction is automatically executed.revoke_confirmationis an action that allows an owner to revoke a confirmation for a transaction.execute_transactionis an action that allows anyone to execute a confirmed transaction.
Events
pub enum Event {
Confirmation {
sender: ActorId,
transaction_id: U256,
},
Revocation {
sender: ActorId,
transaction_id: U256,
},
Submission {
transaction_id: U256,
},
Execution {
transaction_id: U256,
},
OwnerAddition {
owner: ActorId,
},
OwnerRemoval {
owner: ActorId,
},
OwnerReplace {
old_owner: ActorId,
new_owner: ActorId,
},
RequirementChange(U256),
}Confirmationis an event that occurs when theConfirmTransactionaction is successfully used.Revocationis an event that occurs when theRevokeConfirmationaction is successfully used.Submissionis an event that occurs when theSubmitTransactionaction is successfully used.Executionis an event that occurs when theExecuteTransactionaction is successfully used.OwnerAdditionis an event that occurs when the wallet uses theAddOwneraction successfully.OwnerRemovalis an event that occurs when the wallet uses theRemoveOwneraction successfully.OwnerReplaceis an event that occurs when the wallet uses theReplaceOwneraction successfully.RequirementChangeis an event that occurs when the wallet uses theChangeRequiredConfirmationsCountaction successfully.
Query
pub fn get_state(&self) -> State {
self.get().clone().into()
}Where State is defined as follows:
pub struct State {
pub transactions: Vec<(TransactionId, Transaction)>,
pub confirmations: Vec<(TransactionId, Vec<ActorId>)>,
pub owners: Vec<ActorId>,
pub required: u32,
pub transaction_count: U256,
}Source Code
The source code of this example of a Multisig Wallet program and an implementation of its testing is available on GitHub.
See also an example of the program testing implementation based on gtest: multisig-wallet/tests.
For more details about testing program written on Vara, refer to the Program Testing article.