multisig_account - [mainnet]
Enhanced multisig account standard on Aptos. This is different from the native multisig scheme support enforced via the account’s auth key.
This module allows creating a flexible and powerful multisig account with seamless support for updating owners without changing the auth key. Users can choose to store transaction payloads waiting for owner signatures on chain or off chain (primary consideration is decentralization/transparency vs gas cost).
The multisig account is a resource account underneath. By default, it has no auth key and can only be controlled via the special multisig transaction flow. However, owners can create a transaction to change the auth key to match a private key off chain if so desired.
Transactions need to be executed in order of creation, similar to transactions for a normal Aptos account (enforced with account nonce).
The flow is like below:
- Owners can create a new multisig account by calling create (signer is default single owner) or with create_with_owners where multiple initial owner addresses can be specified. This is different (and easier) from the native multisig scheme where the owners’ public keys have to be specified. Here, only addresses are needed.
- Owners can be added/removed any time by calling add_owners or remove_owners. The transactions to do still need to follow the k-of-n scheme specified for the multisig account.
- To create a new transaction, an owner can call create_transaction with the transaction payload. This will store the full transaction payload on chain, which adds decentralization (censorship is not possible as the data is available on chain) and makes it easier to fetch all transactions waiting for execution. If saving gas is desired, an owner can alternatively call create_transaction_with_hash where only the payload hash is stored. Later execution will be verified using the hash. Only owners can create transactions and a transaction id (incremeting id) will be assigned.
- To approve or reject a transaction, other owners can call approve() or reject() with the transaction id.
- If there are enough approvals, any owner can execute the transaction using the special MultisigTransaction type with the transaction id if the full payload is already stored on chain or with the transaction payload if only a hash is stored. Transaction execution will first check with this module that the transaction payload has gotten enough signatures. If so, it will be executed as the multisig account. The owner who executes will pay for gas.
- If there are enough rejections, any owner can finalize the rejection by calling execute_rejected_transaction().
Note that this multisig account model is not designed to use with a large number of owners. The more owners there are, the more expensive voting on transactions will become. If a large number of owners is designed, such as in a flat governance structure, clients are encouraged to write their own modules on top of this multisig account module and implement the governance voting logic on top.
use 0x1::account;use 0x1::aptos_coin;use 0x1::bcs;use 0x1::chain_id;use 0x1::coin;use 0x1::create_signer;use 0x1::error;use 0x1::event;use 0x1::features;use 0x1::hash;use 0x1::option;use 0x1::signer;use 0x1::simple_map;use 0x1::string;use 0x1::table;use 0x1::timestamp;use 0x1::vector;
Constants
const ZERO_AUTH_KEY: vector<u8> = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
The salt used to create a resource account during multisig account creation. This is used to avoid conflicts with other modules that also create resource accounts with the same owner account.
const DOMAIN_SEPARATOR: vector<u8> = [97, 112, 116, 111, 115, 95, 102, 114, 97, 109, 101, 119, 111, 114, 107, 58, 58, 109, 117, 108, 116, 105, 115, 105, 103, 95, 97, 99, 99, 111, 117, 110, 116];
Specified account is not a multisig account.
const EACCOUNT_NOT_MULTISIG: u64 = 2002;
The specified metadata contains duplicate attributes (keys).
const EDUPLICATE_METADATA_KEY: u64 = 16;
Owner list cannot contain the same address more than once.
const EDUPLICATE_OWNER: u64 = 1;
Payload hash must be exactly 32 bytes (sha3-256).
const EINVALID_PAYLOAD_HASH: u64 = 12;
The sequence number provided is invalid. It must be between [1, next pending transaction - 1].
const EINVALID_SEQUENCE_NUMBER: u64 = 17;
Number of signatures required must be more than zero and at most the total number of owners.
const EINVALID_SIGNATURES_REQUIRED: u64 = 11;
The number of pending transactions has exceeded the maximum allowed.
const EMAX_PENDING_TRANSACTIONS_EXCEEDED: u64 = 19;
Multisig accounts has not been enabled on this current network yet.
const EMULTISIG_ACCOUNTS_NOT_ENABLED_YET: u64 = 14;
The multisig v2 enhancement feature is not enabled.
const EMULTISIG_V2_ENHANCEMENT_NOT_ENABLED: u64 = 20;
Transaction has not received enough approvals to be executed.
const ENOT_ENOUGH_APPROVALS: u64 = 2009;
Multisig account must have at least one owner.
const ENOT_ENOUGH_OWNERS: u64 = 5;
Transaction has not received enough rejections to be officially rejected.
const ENOT_ENOUGH_REJECTIONS: u64 = 10;
Account executing this operation is not an owner of the multisig account.
const ENOT_OWNER: u64 = 2003;
The number of metadata keys and values don’t match.
const ENUMBER_OF_METADATA_KEYS_AND_VALUES_DONT_MATCH: u64 = 15;
Provided owners to remove and new owners overlap.
const EOWNERS_TO_REMOVE_NEW_OWNERS_OVERLAP: u64 = 18;
The multisig account itself cannot be an owner.
const EOWNER_CANNOT_BE_MULTISIG_ACCOUNT_ITSELF: u64 = 13;
Transaction payload cannot be empty.
const EPAYLOAD_CANNOT_BE_EMPTY: u64 = 4;
Provided target function does not match the payload stored in the on-chain transaction.
const EPAYLOAD_DOES_NOT_MATCH: u64 = 2010;
Provided target function does not match the hash stored in the on-chain transaction.
const EPAYLOAD_DOES_NOT_MATCH_HASH: u64 = 2008;
Transaction with specified id cannot be found.
const ETRANSACTION_NOT_FOUND: u64 = 2006;
const MAX_PENDING_TRANSACTIONS: u64 = 20;
Structs
MultisigTransaction
A transaction to be executed in a multisig account. This must contain either the full transaction payload or its hash (stored as bytes).
struct MultisigTransaction has copy, drop, store
Fields
-
payload: option::Option<vector<u8>>
-
payload_hash: option::Option<vector<u8>>
-
votes: simple_map::SimpleMap<address, bool>
-
creator: address
-
creation_time_secs: u64
ExecutionError
Contains information about execution failure.
struct ExecutionError has copy, drop, store
Fields
-
abort_location: string::String
-
error_type: string::String
-
error_code: u64
MultisigAccountCreationMessage
Used only for verifying multisig account creation on top of existing accounts.
struct MultisigAccountCreationMessage has copy, drop
Fields
MultisigAccountCreationWithAuthKeyRevocationMessage
Used only for verifying multisig account creation on top of existing accounts and rotating the auth key to 0x0.
struct MultisigAccountCreationWithAuthKeyRevocationMessage has copy, drop
Fields
AddOwnersEvent
Event emitted when new owners are added to the multisig account.
struct AddOwnersEvent has drop, store
Fields
-
owners_added: vector<address>
AddOwners
#[event]struct AddOwners has drop, store
Fields
-
multisig_account: address
-
owners_added: vector<address>
RemoveOwnersEvent
Event emitted when new owners are removed from the multisig account.
struct RemoveOwnersEvent has drop, store
Fields
-
owners_removed: vector<address>
RemoveOwners
#[event]struct RemoveOwners has drop, store
Fields
-
multisig_account: address
-
owners_removed: vector<address>
UpdateSignaturesRequiredEvent
Event emitted when the number of signatures required is updated.
struct UpdateSignaturesRequiredEvent has drop, store
Fields
-
old_num_signatures_required: u64
-
new_num_signatures_required: u64
UpdateSignaturesRequired
#[event]struct UpdateSignaturesRequired has drop, store
Fields
-
multisig_account: address
-
old_num_signatures_required: u64
-
new_num_signatures_required: u64
CreateTransactionEvent
Event emitted when a transaction is created.
struct CreateTransactionEvent has drop, store
Fields
-
creator: address
-
sequence_number: u64
-
transaction: multisig_account::MultisigTransaction
CreateTransaction
#[event]struct CreateTransaction has drop, store
Fields
-
multisig_account: address
-
creator: address
-
sequence_number: u64
-
transaction: multisig_account::MultisigTransaction
VoteEvent
Event emitted when an owner approves or rejects a transaction.
struct VoteEvent has drop, store
Fields
-
owner: address
-
sequence_number: u64
-
approved: bool
Vote
#[event]struct Vote has drop, store
Fields
-
multisig_account: address
-
owner: address
-
sequence_number: u64
-
approved: bool
ExecuteRejectedTransactionEvent
Event emitted when a transaction is officially rejected because the number of rejections has reached the number of signatures required.
struct ExecuteRejectedTransactionEvent has drop, store
Fields
-
sequence_number: u64
-
num_rejections: u64
-
executor: address
ExecuteRejectedTransaction
#[event]struct ExecuteRejectedTransaction has drop, store
Fields
-
multisig_account: address
-
sequence_number: u64
-
num_rejections: u64
-
executor: address
TransactionExecutionSucceededEvent
Event emitted when a transaction is executed.
struct TransactionExecutionSucceededEvent has drop, store
Fields
-
executor: address
-
sequence_number: u64
-
transaction_payload: vector<u8>
-
num_approvals: u64
TransactionExecutionSucceeded
#[event]struct TransactionExecutionSucceeded has drop, store
Fields
-
multisig_account: address
-
executor: address
-
sequence_number: u64
-
transaction_payload: vector<u8>
-
num_approvals: u64
TransactionExecutionFailedEvent
Event emitted when a transaction’s execution failed.
struct TransactionExecutionFailedEvent has drop, store
Fields
-
executor: address
-
sequence_number: u64
-
transaction_payload: vector<u8>
-
num_approvals: u64
-
execution_error: multisig_account::ExecutionError
TransactionExecutionFailed
#[event]struct TransactionExecutionFailed has drop, store
Fields
-
multisig_account: address
-
executor: address
-
sequence_number: u64
-
transaction_payload: vector<u8>
-
num_approvals: u64
-
execution_error: multisig_account::ExecutionError
MetadataUpdatedEvent
Event emitted when a transaction’s metadata is updated.
struct MetadataUpdatedEvent has drop, store
Fields
-
old_metadata: simple_map::SimpleMap<string::String, vector<u8>>
-
new_metadata: simple_map::SimpleMap<string::String, vector<u8>>
MetadataUpdated
#[event]struct MetadataUpdated has drop, store
Fields
-
multisig_account: address
-
old_metadata: simple_map::SimpleMap<string::String, vector<u8>>
-
new_metadata: simple_map::SimpleMap<string::String, vector<u8>>
Resources
MultisigAccount
Represents a multisig account’s configurations and transactions. This will be stored in the multisig account (created as a resource account separate from any owner accounts).
struct MultisigAccount has key
Fields
-
owners: vector<address>
-
num_signatures_required: u64
-
transactions: table::Table<u64, multisig_account::MultisigTransaction>
-
last_executed_sequence_number: u64
-
next_sequence_number: u64
-
signer_cap: option::Option<account::SignerCapability>
-
metadata: simple_map::SimpleMap<string::String, vector<u8>>
-
add_owners_events: event::EventHandle<multisig_account::AddOwnersEvent>
-
remove_owners_events: event::EventHandle<multisig_account::RemoveOwnersEvent>
-
update_signature_required_events: event::EventHandle<multisig_account::UpdateSignaturesRequiredEvent>
-
create_transaction_events: event::EventHandle<multisig_account::CreateTransactionEvent>
-
vote_events: event::EventHandle<multisig_account::VoteEvent>
-
execute_rejected_transaction_events: event::EventHandle<multisig_account::ExecuteRejectedTransactionEvent>
-
execute_transaction_events: event::EventHandle<multisig_account::TransactionExecutionSucceededEvent>
-
transaction_execution_failed_events: event::EventHandle<multisig_account::TransactionExecutionFailedEvent>
-
metadata_updated_events: event::EventHandle<multisig_account::MetadataUpdatedEvent>
Functions
metadata
Return the multisig account’s metadata.
#[view]public fun metadata(multisig_account: address): simple_map::SimpleMap<string::String, vector<u8>>
Implementation
public fun metadata(multisig_account: address): SimpleMap<String, vector<u8>> acquires MultisigAccount { borrow_global<MultisigAccount>(multisig_account).metadata}
num_signatures_required
Return the number of signatures required to execute or execute-reject a transaction in the provided multisig account.
#[view]public fun num_signatures_required(multisig_account: address): u64
Implementation
public fun num_signatures_required(multisig_account: address): u64 acquires MultisigAccount { borrow_global<MultisigAccount>(multisig_account).num_signatures_required}
owners
Return a vector of all of the provided multisig account’s owners.
#[view]public fun owners(multisig_account: address): vector<address>
Implementation
public fun owners(multisig_account: address): vector<address> acquires MultisigAccount { borrow_global<MultisigAccount>(multisig_account).owners}
is_owner
Return true if the provided owner is an owner of the provided multisig account.
#[view]public fun is_owner(owner: address, multisig_account: address): bool
Implementation
public fun is_owner(owner: address, multisig_account: address): bool acquires MultisigAccount { vector::contains(&borrow_global<MultisigAccount>(multisig_account).owners, &owner)}
get_transaction
Return the transaction with the given transaction id.
#[view]public fun get_transaction(multisig_account: address, sequence_number: u64): multisig_account::MultisigTransaction
Implementation
public fun get_transaction( multisig_account: address, sequence_number: u64,): MultisigTransaction acquires MultisigAccount { let multisig_account_resource = borrow_global<MultisigAccount>(multisig_account); assert!( sequence_number > 0 && sequence_number < multisig_account_resource.next_sequence_number, error::invalid_argument(EINVALID_SEQUENCE_NUMBER), ); *table::borrow(&multisig_account_resource.transactions, sequence_number)}
get_pending_transactions
Return all pending transactions.
#[view]public fun get_pending_transactions(multisig_account: address): vector<multisig_account::MultisigTransaction>
Implementation
public fun get_pending_transactions( multisig_account: address): vector<MultisigTransaction> acquires MultisigAccount { let pending_transactions: vector<MultisigTransaction> = vector[]; let multisig_account = borrow_global<MultisigAccount>(multisig_account); let i = multisig_account.last_executed_sequence_number + 1; let next_sequence_number = multisig_account.next_sequence_number; while (i < next_sequence_number) { vector::push_back(&mut pending_transactions, *table::borrow(&multisig_account.transactions, i)); i = i + 1; }; pending_transactions}
get_next_transaction_payload
Return the payload for the next transaction in the queue.
#[view]public fun get_next_transaction_payload(multisig_account: address, provided_payload: vector<u8>): vector<u8>
Implementation
public fun get_next_transaction_payload( multisig_account: address, provided_payload: vector<u8>): vector<u8> acquires MultisigAccount { let multisig_account_resource = borrow_global<MultisigAccount>(multisig_account); let sequence_number = multisig_account_resource.last_executed_sequence_number + 1; let transaction = table::borrow(&multisig_account_resource.transactions, sequence_number);
if (option::is_some(&transaction.payload)) { *option::borrow(&transaction.payload) } else { provided_payload }}
can_be_executed
Return true if the transaction with given transaction id can be executed now.
#[view]public fun can_be_executed(multisig_account: address, sequence_number: u64): bool
Implementation
public fun can_be_executed(multisig_account: address, sequence_number: u64): bool acquires MultisigAccount { assert_valid_sequence_number(multisig_account, sequence_number); let (num_approvals, _) = num_approvals_and_rejections(multisig_account, sequence_number); sequence_number == last_resolved_sequence_number(multisig_account) + 1 && num_approvals >= num_signatures_required(multisig_account)}
can_execute
Return true if the owner can execute the transaction with given transaction id now.
#[view]public fun can_execute(owner: address, multisig_account: address, sequence_number: u64): bool
Implementation
public fun can_execute(owner: address, multisig_account: address, sequence_number: u64): bool acquires MultisigAccount { assert_valid_sequence_number(multisig_account, sequence_number); let (num_approvals, _) = num_approvals_and_rejections(multisig_account, sequence_number); if (!has_voted_for_approval(multisig_account, sequence_number, owner)) { num_approvals = num_approvals + 1; }; is_owner(owner, multisig_account) && sequence_number == last_resolved_sequence_number(multisig_account) + 1 && num_approvals >= num_signatures_required(multisig_account)}
can_be_rejected
Return true if the transaction with given transaction id can be officially rejected.
#[view]public fun can_be_rejected(multisig_account: address, sequence_number: u64): bool
Implementation
public fun can_be_rejected(multisig_account: address, sequence_number: u64): bool acquires MultisigAccount { assert_valid_sequence_number(multisig_account, sequence_number); let (_, num_rejections) = num_approvals_and_rejections(multisig_account, sequence_number); sequence_number == last_resolved_sequence_number(multisig_account) + 1 && num_rejections >= num_signatures_required(multisig_account)}
can_reject
Return true if the owner can execute the “rejected” transaction with given transaction id now.
#[view]public fun can_reject(owner: address, multisig_account: address, sequence_number: u64): bool
Implementation
public fun can_reject(owner: address, multisig_account: address, sequence_number: u64): bool acquires MultisigAccount { assert_valid_sequence_number(multisig_account, sequence_number); let (_, num_rejections) = num_approvals_and_rejections(multisig_account, sequence_number); if (!has_voted_for_rejection(multisig_account, sequence_number, owner)) { num_rejections = num_rejections + 1; }; is_owner(owner, multisig_account) && sequence_number == last_resolved_sequence_number(multisig_account) + 1 && num_rejections >= num_signatures_required(multisig_account)}
get_next_multisig_account_address
Return the predicted address for the next multisig account if created from the given creator address.
#[view]public fun get_next_multisig_account_address(creator: address): address
Implementation
public fun get_next_multisig_account_address(creator: address): address { let owner_nonce = account::get_sequence_number(creator); create_resource_address(&creator, create_multisig_account_seed(to_bytes(&owner_nonce)))}
last_resolved_sequence_number
Return the id of the last transaction that was executed (successful or failed) or removed.
#[view]public fun last_resolved_sequence_number(multisig_account: address): u64
Implementation
public fun last_resolved_sequence_number(multisig_account: address): u64 acquires MultisigAccount { let multisig_account_resource = borrow_global<MultisigAccount>(multisig_account); multisig_account_resource.last_executed_sequence_number}
next_sequence_number
Return the id of the next transaction created.
#[view]public fun next_sequence_number(multisig_account: address): u64
Implementation
public fun next_sequence_number(multisig_account: address): u64 acquires MultisigAccount { let multisig_account_resource = borrow_global<MultisigAccount>(multisig_account); multisig_account_resource.next_sequence_number}
vote
Return a bool tuple indicating whether an owner has voted and if so, whether they voted yes or no.
#[view]public fun vote(multisig_account: address, sequence_number: u64, owner: address): (bool, bool)
Implementation
public fun vote( multisig_account: address, sequence_number: u64, owner: address): (bool, bool) acquires MultisigAccount { let multisig_account_resource = borrow_global<MultisigAccount>(multisig_account); assert!( sequence_number > 0 && sequence_number < multisig_account_resource.next_sequence_number, error::invalid_argument(EINVALID_SEQUENCE_NUMBER), ); let transaction = table::borrow(&multisig_account_resource.transactions, sequence_number); let votes = &transaction.votes; let voted = simple_map::contains_key(votes, &owner); let vote = voted && *simple_map::borrow(votes, &owner); (voted, vote)}
available_transaction_queue_capacity
#[view]public fun available_transaction_queue_capacity(multisig_account: address): u64
Implementation
public fun available_transaction_queue_capacity(multisig_account: address): u64 acquires MultisigAccount { let multisig_account_resource = borrow_global<MultisigAccount>(multisig_account); let num_pending_transactions = multisig_account_resource.next_sequence_number - multisig_account_resource.last_executed_sequence_number - 1; if (num_pending_transactions > MAX_PENDING_TRANSACTIONS) { 0 } else { MAX_PENDING_TRANSACTIONS - num_pending_transactions }}
create_with_existing_account_call
Private entry function that creates a new multisig account on top of an existing account.
This offers a migration path for an existing account with any type of auth key.
Note that this does not revoke auth key-based control over the account. Owners should separately rotate the auth key after they are fully migrated to the new multisig account. Alternatively, they can call create_with_existing_account_and_revoke_auth_key_call instead.
entry fun create_with_existing_account_call(multisig_account: &signer, owners: vector<address>, num_signatures_required: u64, metadata_keys: vector<string::String>, metadata_values: vector<vector<u8>>)
Implementation
entry fun create_with_existing_account_call( multisig_account: &signer, owners: vector<address>, num_signatures_required: u64, metadata_keys: vector<String>, metadata_values: vector<vector<u8>>,) acquires MultisigAccount { create_with_owners_internal( multisig_account, owners, num_signatures_required, option::none<SignerCapability>(), metadata_keys, metadata_values, );}
create_with_existing_account
Creates a new multisig account on top of an existing account.
This offers a migration path for an existing account with a multi-ed25519 auth key (native multisig account). In order to ensure a malicious module cannot obtain backdoor control over an existing account, a signed message with a valid signature from the account’s auth key is required.
Note that this does not revoke auth key-based control over the account. Owners should separately rotate the auth key after they are fully migrated to the new multisig account. Alternatively, they can call create_with_existing_account_and_revoke_auth_key instead.
public entry fun create_with_existing_account(multisig_address: address, owners: vector<address>, num_signatures_required: u64, account_scheme: u8, account_public_key: vector<u8>, create_multisig_account_signed_message: vector<u8>, metadata_keys: vector<string::String>, metadata_values: vector<vector<u8>>)
Implementation
public entry fun create_with_existing_account( multisig_address: address, owners: vector<address>, num_signatures_required: u64, account_scheme: u8, account_public_key: vector<u8>, create_multisig_account_signed_message: vector<u8>, metadata_keys: vector<String>, metadata_values: vector<vector<u8>>,) acquires MultisigAccount { // Verify that the `MultisigAccountCreationMessage` has the right information and is signed by the account // owner's key. let proof_challenge = MultisigAccountCreationMessage { chain_id: chain_id::get(), account_address: multisig_address, sequence_number: account::get_sequence_number(multisig_address), owners, num_signatures_required, }; account::verify_signed_message( multisig_address, account_scheme, account_public_key, create_multisig_account_signed_message, proof_challenge, );
// We create the signer for the multisig account here since this is required to add the MultisigAccount resource // This should be safe and authorized because we have verified the signed message from the existing account // that authorizes creating a multisig account with the specified owners and signature threshold. let multisig_account = &create_signer(multisig_address); create_with_owners_internal( multisig_account, owners, num_signatures_required, option::none<SignerCapability>(), metadata_keys, metadata_values, );}
create_with_existing_account_and_revoke_auth_key_call
Private entry function that creates a new multisig account on top of an existing account and immediately rotate the origin auth key to 0x0.
Note: If the original account is a resource account, this does not revoke all control over it as if any SignerCapability of the resource account still exists, it can still be used to generate the signer for the account.
entry fun create_with_existing_account_and_revoke_auth_key_call(multisig_account: &signer, owners: vector<address>, num_signatures_required: u64, metadata_keys: vector<string::String>, metadata_values: vector<vector<u8>>)
Implementation
entry fun create_with_existing_account_and_revoke_auth_key_call( multisig_account: &signer, owners: vector<address>, num_signatures_required: u64, metadata_keys: vector<String>, metadata_values:vector<vector<u8>>,) acquires MultisigAccount { create_with_owners_internal( multisig_account, owners, num_signatures_required, option::none<SignerCapability>(), metadata_keys, metadata_values, );
// Rotate the account's auth key to 0x0, which effectively revokes control via auth key. let multisig_address = address_of(multisig_account); account::rotate_authentication_key_internal(multisig_account, ZERO_AUTH_KEY); // This also needs to revoke any signer capability or rotation capability that exists for the account to // completely remove all access to the account. if (account::is_signer_capability_offered(multisig_address)) { account::revoke_any_signer_capability(multisig_account); }; if (account::is_rotation_capability_offered(multisig_address)) { account::revoke_any_rotation_capability(multisig_account); };}
create_with_existing_account_and_revoke_auth_key
Creates a new multisig account on top of an existing account and immediately rotate the origin auth key to 0x0.
Note: If the original account is a resource account, this does not revoke all control over it as if any SignerCapability of the resource account still exists, it can still be used to generate the signer for the account.
public entry fun create_with_existing_account_and_revoke_auth_key(multisig_address: address, owners: vector<address>, num_signatures_required: u64, account_scheme: u8, account_public_key: vector<u8>, create_multisig_account_signed_message: vector<u8>, metadata_keys: vector<string::String>, metadata_values: vector<vector<u8>>)
Implementation
public entry fun create_with_existing_account_and_revoke_auth_key( multisig_address: address, owners: vector<address>, num_signatures_required: u64, account_scheme: u8, account_public_key: vector<u8>, create_multisig_account_signed_message: vector<u8>, metadata_keys: vector<String>, metadata_values: vector<vector<u8>>,) acquires MultisigAccount { // Verify that the `MultisigAccountCreationMessage` has the right information and is signed by the account // owner's key. let proof_challenge = MultisigAccountCreationWithAuthKeyRevocationMessage { chain_id: chain_id::get(), account_address: multisig_address, sequence_number: account::get_sequence_number(multisig_address), owners, num_signatures_required, }; account::verify_signed_message( multisig_address, account_scheme, account_public_key, create_multisig_account_signed_message, proof_challenge, );
// We create the signer for the multisig account here since this is required to add the MultisigAccount resource // This should be safe and authorized because we have verified the signed message from the existing account // that authorizes creating a multisig account with the specified owners and signature threshold. let multisig_account = &create_signer(multisig_address); create_with_owners_internal( multisig_account, owners, num_signatures_required, option::none<SignerCapability>(), metadata_keys, metadata_values, );
// Rotate the account's auth key to 0x0, which effectively revokes control via auth key. let multisig_address = address_of(multisig_account); account::rotate_authentication_key_internal(multisig_account, ZERO_AUTH_KEY); // This also needs to revoke any signer capability or rotation capability that exists for the account to // completely remove all access to the account. if (account::is_signer_capability_offered(multisig_address)) { account::revoke_any_signer_capability(multisig_account); }; if (account::is_rotation_capability_offered(multisig_address)) { account::revoke_any_rotation_capability(multisig_account); };}
create
Creates a new multisig account and add the signer as a single owner.
public entry fun create(owner: &signer, num_signatures_required: u64, metadata_keys: vector<string::String>, metadata_values: vector<vector<u8>>)
Implementation
public entry fun create( owner: &signer, num_signatures_required: u64, metadata_keys: vector<String>, metadata_values: vector<vector<u8>>,) acquires MultisigAccount { create_with_owners(owner, vector[], num_signatures_required, metadata_keys, metadata_values);}
create_with_owners
Creates a new multisig account with the specified additional owner list and signatures required.
@param additional_owners The owner account who calls this function cannot be in the additional_owners and there cannot be any duplicate owners in the list. @param num_signatures_required The number of signatures required to execute a transaction. Must be at least 1 and at most the total number of owners.
public entry fun create_with_owners(owner: &signer, additional_owners: vector<address>, num_signatures_required: u64, metadata_keys: vector<string::String>, metadata_values: vector<vector<u8>>)
Implementation
public entry fun create_with_owners( owner: &signer, additional_owners: vector<address>, num_signatures_required: u64, metadata_keys: vector<String>, metadata_values: vector<vector<u8>>,) acquires MultisigAccount { let (multisig_account, multisig_signer_cap) = create_multisig_account(owner); vector::push_back(&mut additional_owners, address_of(owner)); create_with_owners_internal( &multisig_account, additional_owners, num_signatures_required, option::some(multisig_signer_cap), metadata_keys, metadata_values, );}
create_with_owners_then_remove_bootstrapper
Like create_with_owners
, but removes the calling account after creation.
This is for creating a vanity multisig account from a bootstrapping account that should not be an owner after the vanity multisig address has been secured.
public entry fun create_with_owners_then_remove_bootstrapper(bootstrapper: &signer, owners: vector<address>, num_signatures_required: u64, metadata_keys: vector<string::String>, metadata_values: vector<vector<u8>>)
Implementation
public entry fun create_with_owners_then_remove_bootstrapper( bootstrapper: &signer, owners: vector<address>, num_signatures_required: u64, metadata_keys: vector<String>, metadata_values: vector<vector<u8>>,) acquires MultisigAccount { let bootstrapper_address = address_of(bootstrapper); create_with_owners( bootstrapper, owners, num_signatures_required, metadata_keys, metadata_values ); update_owner_schema( get_next_multisig_account_address(bootstrapper_address), vector[], vector[bootstrapper_address], option::none() );}
create_with_owners_internal
fun create_with_owners_internal(multisig_account: &signer, owners: vector<address>, num_signatures_required: u64, multisig_account_signer_cap: option::Option<account::SignerCapability>, metadata_keys: vector<string::String>, metadata_values: vector<vector<u8>>)
Implementation
fun create_with_owners_internal( multisig_account: &signer, owners: vector<address>, num_signatures_required: u64, multisig_account_signer_cap: Option<SignerCapability>, metadata_keys: vector<String>, metadata_values: vector<vector<u8>>,) acquires MultisigAccount { assert!(features::multisig_accounts_enabled(), error::unavailable(EMULTISIG_ACCOUNTS_NOT_ENABLED_YET)); assert!( num_signatures_required > 0 && num_signatures_required <= vector::length(&owners), error::invalid_argument(EINVALID_SIGNATURES_REQUIRED), );
let multisig_address = address_of(multisig_account); validate_owners(&owners, multisig_address); move_to(multisig_account, MultisigAccount { owners, num_signatures_required, transactions: table::new<u64, MultisigTransaction>(), metadata: simple_map::create<String, vector<u8>>(), // First transaction will start at id 1 instead of 0. last_executed_sequence_number: 0, next_sequence_number: 1, signer_cap: multisig_account_signer_cap, add_owners_events: new_event_handle<AddOwnersEvent>(multisig_account), remove_owners_events: new_event_handle<RemoveOwnersEvent>(multisig_account), update_signature_required_events: new_event_handle<UpdateSignaturesRequiredEvent>(multisig_account), create_transaction_events: new_event_handle<CreateTransactionEvent>(multisig_account), vote_events: new_event_handle<VoteEvent>(multisig_account), execute_rejected_transaction_events: new_event_handle<ExecuteRejectedTransactionEvent>(multisig_account), execute_transaction_events: new_event_handle<TransactionExecutionSucceededEvent>(multisig_account), transaction_execution_failed_events: new_event_handle<TransactionExecutionFailedEvent>(multisig_account), metadata_updated_events: new_event_handle<MetadataUpdatedEvent>(multisig_account), });
update_metadata_internal(multisig_account, metadata_keys, metadata_values, false);}
add_owner
Similar to add_owners, but only allow adding one owner.
entry fun add_owner(multisig_account: &signer, new_owner: address)
Implementation
entry fun add_owner(multisig_account: &signer, new_owner: address) acquires MultisigAccount { add_owners(multisig_account, vector[new_owner]);}
add_owners
Add new owners to the multisig account. This can only be invoked by the multisig account itself, through the proposal flow.
Note that this function is not public so it can only be invoked directly instead of via a module or script. This ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to maliciously alter the owners list.
entry fun add_owners(multisig_account: &signer, new_owners: vector<address>)
Implementation
entry fun add_owners( multisig_account: &signer, new_owners: vector<address>) acquires MultisigAccount { update_owner_schema( address_of(multisig_account), new_owners, vector[], option::none() );}
add_owners_and_update_signatures_required
Add owners then update number of signatures required, in a single operation.
entry fun add_owners_and_update_signatures_required(multisig_account: &signer, new_owners: vector<address>, new_num_signatures_required: u64)
Implementation
entry fun add_owners_and_update_signatures_required( multisig_account: &signer, new_owners: vector<address>, new_num_signatures_required: u64) acquires MultisigAccount { update_owner_schema( address_of(multisig_account), new_owners, vector[], option::some(new_num_signatures_required) );}
remove_owner
Similar to remove_owners, but only allow removing one owner.
entry fun remove_owner(multisig_account: &signer, owner_to_remove: address)
Implementation
entry fun remove_owner( multisig_account: &signer, owner_to_remove: address) acquires MultisigAccount { remove_owners(multisig_account, vector[owner_to_remove]);}
remove_owners
Remove owners from the multisig account. This can only be invoked by the multisig account itself, through the proposal flow.
This function skips any owners who are not in the multisig account’s list of owners. Note that this function is not public so it can only be invoked directly instead of via a module or script. This ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to maliciously alter the owners list.
entry fun remove_owners(multisig_account: &signer, owners_to_remove: vector<address>)
Implementation
entry fun remove_owners( multisig_account: &signer, owners_to_remove: vector<address>) acquires MultisigAccount { update_owner_schema( address_of(multisig_account), vector[], owners_to_remove, option::none() );}
swap_owner
Swap an owner in for an old one, without changing required signatures.
entry fun swap_owner(multisig_account: &signer, to_swap_in: address, to_swap_out: address)
Implementation
entry fun swap_owner( multisig_account: &signer, to_swap_in: address, to_swap_out: address) acquires MultisigAccount { update_owner_schema( address_of(multisig_account), vector[to_swap_in], vector[to_swap_out], option::none() );}
swap_owners
Swap owners in and out, without changing required signatures.
entry fun swap_owners(multisig_account: &signer, to_swap_in: vector<address>, to_swap_out: vector<address>)
Implementation
entry fun swap_owners( multisig_account: &signer, to_swap_in: vector<address>, to_swap_out: vector<address>) acquires MultisigAccount { update_owner_schema( address_of(multisig_account), to_swap_in, to_swap_out, option::none() );}
swap_owners_and_update_signatures_required
Swap owners in and out, updating number of required signatures.
entry fun swap_owners_and_update_signatures_required(multisig_account: &signer, new_owners: vector<address>, owners_to_remove: vector<address>, new_num_signatures_required: u64)
Implementation
entry fun swap_owners_and_update_signatures_required( multisig_account: &signer, new_owners: vector<address>, owners_to_remove: vector<address>, new_num_signatures_required: u64) acquires MultisigAccount { update_owner_schema( address_of(multisig_account), new_owners, owners_to_remove, option::some(new_num_signatures_required) );}
update_signatures_required
Update the number of signatures required to execute transaction in the specified multisig account.
This can only be invoked by the multisig account itself, through the proposal flow. Note that this function is not public so it can only be invoked directly instead of via a module or script. This ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to maliciously alter the number of signatures required.
entry fun update_signatures_required(multisig_account: &signer, new_num_signatures_required: u64)
Implementation
entry fun update_signatures_required( multisig_account: &signer, new_num_signatures_required: u64) acquires MultisigAccount { update_owner_schema( address_of(multisig_account), vector[], vector[], option::some(new_num_signatures_required) );}
update_metadata
Allow the multisig account to update its own metadata. Note that this overrides the entire existing metadata. If any attributes are not specified in the metadata, they will be removed!
This can only be invoked by the multisig account itself, through the proposal flow. Note that this function is not public so it can only be invoked directly instead of via a module or script. This ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to maliciously alter the number of signatures required.
entry fun update_metadata(multisig_account: &signer, keys: vector<string::String>, values: vector<vector<u8>>)
Implementation
entry fun update_metadata( multisig_account: &signer, keys: vector<String>, values: vector<vector<u8>>) acquires MultisigAccount { update_metadata_internal(multisig_account, keys, values, true);}
update_metadata_internal
fun update_metadata_internal(multisig_account: &signer, keys: vector<string::String>, values: vector<vector<u8>>, emit_event: bool)
Implementation
fun update_metadata_internal( multisig_account: &signer, keys: vector<String>, values: vector<vector<u8>>, emit_event: bool,) acquires MultisigAccount { let num_attributes = vector::length(&keys); assert!( num_attributes == vector::length(&values), error::invalid_argument(ENUMBER_OF_METADATA_KEYS_AND_VALUES_DONT_MATCH), );
let multisig_address = address_of(multisig_account); assert_multisig_account_exists(multisig_address); let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_address); let old_metadata = multisig_account_resource.metadata; multisig_account_resource.metadata = simple_map::create<String, vector<u8>>(); let metadata = &mut multisig_account_resource.metadata; let i = 0; while (i < num_attributes) { let key = *vector::borrow(&keys, i); let value = *vector::borrow(&values, i); assert!( !simple_map::contains_key(metadata, &key), error::invalid_argument(EDUPLICATE_METADATA_KEY), );
simple_map::add(metadata, key, value); i = i + 1; };
if (emit_event) { if (std::features::module_event_migration_enabled()) { emit( MetadataUpdated { multisig_account: multisig_address, old_metadata, new_metadata: multisig_account_resource.metadata, } ) } else { emit_event( &mut multisig_account_resource.metadata_updated_events, MetadataUpdatedEvent { old_metadata, new_metadata: multisig_account_resource.metadata, } ); }; };}
create_transaction
Create a multisig transaction, which will have one approval initially (from the creator).
public entry fun create_transaction(owner: &signer, multisig_account: address, payload: vector<u8>)
Implementation
public entry fun create_transaction( owner: &signer, multisig_account: address, payload: vector<u8>,) acquires MultisigAccount { assert!(vector::length(&payload) > 0, error::invalid_argument(EPAYLOAD_CANNOT_BE_EMPTY));
assert_multisig_account_exists(multisig_account); assert_is_owner(owner, multisig_account);
let creator = address_of(owner); let transaction = MultisigTransaction { payload: option::some(payload), payload_hash: option::none<vector<u8>>(), votes: simple_map::create<address, bool>(), creator, creation_time_secs: now_seconds(), }; add_transaction(creator, multisig_account, transaction);}
create_transaction_with_hash
Create a multisig transaction with a transaction hash instead of the full payload. This means the payload will be stored off chain for gas saving. Later, during execution, the executor will need to provide the full payload, which will be validated against the hash stored on-chain.
public entry fun create_transaction_with_hash(owner: &signer, multisig_account: address, payload_hash: vector<u8>)
Implementation
public entry fun create_transaction_with_hash( owner: &signer, multisig_account: address, payload_hash: vector<u8>,) acquires MultisigAccount { // Payload hash is a sha3-256 hash, so it must be exactly 32 bytes. assert!(vector::length(&payload_hash) == 32, error::invalid_argument(EINVALID_PAYLOAD_HASH));
assert_multisig_account_exists(multisig_account); assert_is_owner(owner, multisig_account);
let creator = address_of(owner); let transaction = MultisigTransaction { payload: option::none<vector<u8>>(), payload_hash: option::some(payload_hash), votes: simple_map::create<address, bool>(), creator, creation_time_secs: now_seconds(), }; add_transaction(creator, multisig_account, transaction);}
approve_transaction
Approve a multisig transaction.
public entry fun approve_transaction(owner: &signer, multisig_account: address, sequence_number: u64)
Implementation
public entry fun approve_transaction( owner: &signer, multisig_account: address, sequence_number: u64) acquires MultisigAccount { vote_transanction(owner, multisig_account, sequence_number, true);}
reject_transaction
Reject a multisig transaction.
public entry fun reject_transaction(owner: &signer, multisig_account: address, sequence_number: u64)
Implementation
public entry fun reject_transaction( owner: &signer, multisig_account: address, sequence_number: u64) acquires MultisigAccount { vote_transanction(owner, multisig_account, sequence_number, false);}
vote_transanction
Generic function that can be used to either approve or reject a multisig transaction Retained for backward compatibility: the function with the typographical error in its name will continue to be an accessible entry point.
public entry fun vote_transanction(owner: &signer, multisig_account: address, sequence_number: u64, approved: bool)
Implementation
public entry fun vote_transanction( owner: &signer, multisig_account: address, sequence_number: u64, approved: bool) acquires MultisigAccount { assert_multisig_account_exists(multisig_account); let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account); assert_is_owner_internal(owner, multisig_account_resource);
assert!( table::contains(&multisig_account_resource.transactions, sequence_number), error::not_found(ETRANSACTION_NOT_FOUND), ); let transaction = table::borrow_mut(&mut multisig_account_resource.transactions, sequence_number); let votes = &mut transaction.votes; let owner_addr = address_of(owner);
if (simple_map::contains_key(votes, &owner_addr)) { *simple_map::borrow_mut(votes, &owner_addr) = approved; } else { simple_map::add(votes, owner_addr, approved); };
if (std::features::module_event_migration_enabled()) { emit( Vote { multisig_account, owner: owner_addr, sequence_number, approved, } ); } else { emit_event( &mut multisig_account_resource.vote_events, VoteEvent { owner: owner_addr, sequence_number, approved, } ); };}
vote_transaction
Generic function that can be used to either approve or reject a multisig transaction
public entry fun vote_transaction(owner: &signer, multisig_account: address, sequence_number: u64, approved: bool)
Implementation
public entry fun vote_transaction( owner: &signer, multisig_account: address, sequence_number: u64, approved: bool) acquires MultisigAccount { assert!(features::multisig_v2_enhancement_feature_enabled(), error::invalid_state(EMULTISIG_V2_ENHANCEMENT_NOT_ENABLED)); vote_transanction(owner, multisig_account, sequence_number, approved);}
vote_transactions
Generic function that can be used to either approve or reject a batch of transactions within a specified range.
public entry fun vote_transactions(owner: &signer, multisig_account: address, starting_sequence_number: u64, final_sequence_number: u64, approved: bool)
Implementation
public entry fun vote_transactions( owner: &signer, multisig_account: address, starting_sequence_number: u64, final_sequence_number: u64, approved: bool) acquires MultisigAccount { assert!(features::multisig_v2_enhancement_feature_enabled(), error::invalid_state(EMULTISIG_V2_ENHANCEMENT_NOT_ENABLED)); let sequence_number = starting_sequence_number; while(sequence_number <= final_sequence_number) { vote_transanction(owner, multisig_account, sequence_number, approved); sequence_number = sequence_number + 1; }}
execute_rejected_transaction
Remove the next transaction if it has sufficient owner rejections.
public entry fun execute_rejected_transaction(owner: &signer, multisig_account: address)
Implementation
public entry fun execute_rejected_transaction( owner: &signer, multisig_account: address,) acquires MultisigAccount { assert_multisig_account_exists(multisig_account); assert_is_owner(owner, multisig_account);
let sequence_number = last_resolved_sequence_number(multisig_account) + 1; let owner_addr = address_of(owner); if (features::multisig_v2_enhancement_feature_enabled()) { // Implicitly vote for rejection if the owner has not voted for rejection yet. if (!has_voted_for_rejection(multisig_account, sequence_number, owner_addr)) { reject_transaction(owner, multisig_account, sequence_number); } };
let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account); let (_, num_rejections) = remove_executed_transaction(multisig_account_resource); assert!( num_rejections >= multisig_account_resource.num_signatures_required, error::invalid_state(ENOT_ENOUGH_REJECTIONS), );
if (std::features::module_event_migration_enabled()) { emit( ExecuteRejectedTransaction { multisig_account, sequence_number, num_rejections, executor: address_of(owner), } ); } else { emit_event( &mut multisig_account_resource.execute_rejected_transaction_events, ExecuteRejectedTransactionEvent { sequence_number, num_rejections, executor: owner_addr, } ); };}
execute_rejected_transactions
Remove the next transactions until the final_sequence_number if they have sufficient owner rejections.
public entry fun execute_rejected_transactions(owner: &signer, multisig_account: address, final_sequence_number: u64)
Implementation
public entry fun execute_rejected_transactions( owner: &signer, multisig_account: address, final_sequence_number: u64,) acquires MultisigAccount { assert!(features::multisig_v2_enhancement_feature_enabled(), error::invalid_state(EMULTISIG_V2_ENHANCEMENT_NOT_ENABLED)); assert!(last_resolved_sequence_number(multisig_account) < final_sequence_number, error::invalid_argument(EINVALID_SEQUENCE_NUMBER)); assert!(final_sequence_number < next_sequence_number(multisig_account), error::invalid_argument(EINVALID_SEQUENCE_NUMBER)); while(last_resolved_sequence_number(multisig_account) < final_sequence_number) { execute_rejected_transaction(owner, multisig_account); }}
validate_multisig_transaction
Called by the VM as part of transaction prologue, which is invoked during mempool transaction validation and as the first step of transaction execution.
Transaction payload is optional if it’s already stored on chain for the transaction.
fun validate_multisig_transaction(owner: &signer, multisig_account: address, payload: vector<u8>)
Implementation
fun validate_multisig_transaction( owner: &signer, multisig_account: address, payload: vector<u8>) acquires MultisigAccount { assert_multisig_account_exists(multisig_account); assert_is_owner(owner, multisig_account); let sequence_number = last_resolved_sequence_number(multisig_account) + 1; assert_transaction_exists(multisig_account, sequence_number);
if (features::multisig_v2_enhancement_feature_enabled()) { assert!( can_execute(address_of(owner), multisig_account, sequence_number), error::invalid_argument(ENOT_ENOUGH_APPROVALS), ); } else { assert!( can_be_executed(multisig_account, sequence_number), error::invalid_argument(ENOT_ENOUGH_APPROVALS), ); };
// If the transaction payload is not stored on chain, verify that the provided payload matches the hashes stored // on chain. let multisig_account_resource = borrow_global<MultisigAccount>(multisig_account); let transaction = table::borrow(&multisig_account_resource.transactions, sequence_number); if (option::is_some(&transaction.payload_hash)) { let payload_hash = option::borrow(&transaction.payload_hash); assert!( sha3_256(payload) == *payload_hash, error::invalid_argument(EPAYLOAD_DOES_NOT_MATCH_HASH), ); };
// If the transaction payload is stored on chain and there is a provided payload, // verify that the provided payload matches the stored payload. if (features::abort_if_multisig_payload_mismatch_enabled() && option::is_some(&transaction.payload) && !vector::is_empty(&payload) ) { let stored_payload = option::borrow(&transaction.payload); assert!( payload == *stored_payload, error::invalid_argument(EPAYLOAD_DOES_NOT_MATCH), ); }}
successful_transaction_execution_cleanup
Post-execution cleanup for a successful multisig transaction execution. This function is private so no other code can call this beside the VM itself as part of MultisigTransaction.
fun successful_transaction_execution_cleanup(executor: address, multisig_account: address, transaction_payload: vector<u8>)
Implementation
fun successful_transaction_execution_cleanup( executor: address, multisig_account: address, transaction_payload: vector<u8>,) acquires MultisigAccount { let num_approvals = transaction_execution_cleanup_common(executor, multisig_account); let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account); if (std::features::module_event_migration_enabled()) { emit( TransactionExecutionSucceeded { multisig_account, sequence_number: multisig_account_resource.last_executed_sequence_number, transaction_payload, num_approvals, executor, } ); } else { emit_event( &mut multisig_account_resource.execute_transaction_events, TransactionExecutionSucceededEvent { sequence_number: multisig_account_resource.last_executed_sequence_number, transaction_payload, num_approvals, executor, } ); };}
failed_transaction_execution_cleanup
Post-execution cleanup for a failed multisig transaction execution. This function is private so no other code can call this beside the VM itself as part of MultisigTransaction.
fun failed_transaction_execution_cleanup(executor: address, multisig_account: address, transaction_payload: vector<u8>, execution_error: multisig_account::ExecutionError)
Implementation
fun failed_transaction_execution_cleanup( executor: address, multisig_account: address, transaction_payload: vector<u8>, execution_error: ExecutionError,) acquires MultisigAccount { let num_approvals = transaction_execution_cleanup_common(executor, multisig_account); let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account); if (std::features::module_event_migration_enabled()) { emit( TransactionExecutionFailed { multisig_account, executor, sequence_number: multisig_account_resource.last_executed_sequence_number, transaction_payload, num_approvals, execution_error, } ); } else { emit_event( &mut multisig_account_resource.transaction_execution_failed_events, TransactionExecutionFailedEvent { executor, sequence_number: multisig_account_resource.last_executed_sequence_number, transaction_payload, num_approvals, execution_error, } ); };}
transaction_execution_cleanup_common
fun transaction_execution_cleanup_common(executor: address, multisig_account: address): u64
Implementation
inline fun transaction_execution_cleanup_common(executor: address, multisig_account: address): u64 acquires MultisigAccount { let sequence_number = last_resolved_sequence_number(multisig_account) + 1; let implicit_approval = !has_voted_for_approval(multisig_account, sequence_number, executor);
let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account); let (num_approvals, _) = remove_executed_transaction(multisig_account_resource);
if (features::multisig_v2_enhancement_feature_enabled() && implicit_approval) { if (std::features::module_event_migration_enabled()) { emit( Vote { multisig_account, owner: executor, sequence_number, approved: true, } ); } else { emit_event( &mut multisig_account_resource.vote_events, VoteEvent { owner: executor, sequence_number, approved: true, } ); }; num_approvals = num_approvals + 1; };
num_approvals}
remove_executed_transaction
fun remove_executed_transaction(multisig_account_resource: &mut multisig_account::MultisigAccount): (u64, u64)
Implementation
fun remove_executed_transaction(multisig_account_resource: &mut MultisigAccount): (u64, u64) { let sequence_number = multisig_account_resource.last_executed_sequence_number + 1; let transaction = table::remove(&mut multisig_account_resource.transactions, sequence_number); multisig_account_resource.last_executed_sequence_number = sequence_number; num_approvals_and_rejections_internal(&multisig_account_resource.owners, &transaction)}
add_transaction
fun add_transaction(creator: address, multisig_account: address, transaction: multisig_account::MultisigTransaction)
Implementation
inline fun add_transaction( creator: address, multisig_account: address, transaction: MultisigTransaction) { if (features::multisig_v2_enhancement_feature_enabled()) { assert!( available_transaction_queue_capacity(multisig_account) > 0, error::invalid_state(EMAX_PENDING_TRANSACTIONS_EXCEEDED) ); };
let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
// The transaction creator also automatically votes for the transaction. simple_map::add(&mut transaction.votes, creator, true);
let sequence_number = multisig_account_resource.next_sequence_number; multisig_account_resource.next_sequence_number = sequence_number + 1; table::add(&mut multisig_account_resource.transactions, sequence_number, transaction); if (std::features::module_event_migration_enabled()) { emit( CreateTransaction { multisig_account: multisig_account, creator, sequence_number, transaction } ); } else { emit_event( &mut multisig_account_resource.create_transaction_events, CreateTransactionEvent { creator, sequence_number, transaction }, ); };}
create_multisig_account
fun create_multisig_account(owner: &signer): (signer, account::SignerCapability)
Implementation
fun create_multisig_account(owner: &signer): (signer, SignerCapability) { let owner_nonce = account::get_sequence_number(address_of(owner)); let (multisig_signer, multisig_signer_cap) = account::create_resource_account(owner, create_multisig_account_seed(to_bytes(&owner_nonce))); // Register the account to receive APT as this is not done by default as part of the resource account creation // flow. if (!coin::is_account_registered<AptosCoin>(address_of(&multisig_signer))) { coin::register<AptosCoin>(&multisig_signer); };
(multisig_signer, multisig_signer_cap)}
create_multisig_account_seed
fun create_multisig_account_seed(seed: vector<u8>): vector<u8>
Implementation
fun create_multisig_account_seed(seed: vector<u8>): vector<u8> { // Generate a seed that will be used to create the resource account that hosts the multisig account. let multisig_account_seed = vector::empty<u8>(); vector::append(&mut multisig_account_seed, DOMAIN_SEPARATOR); vector::append(&mut multisig_account_seed, seed);
multisig_account_seed}
validate_owners
fun validate_owners(owners: &vector<address>, multisig_account: address)
Implementation
fun validate_owners(owners: &vector<address>, multisig_account: address) { let distinct_owners: vector<address> = vector[]; vector::for_each_ref(owners, |owner| { let owner = *owner; assert!(owner != multisig_account, error::invalid_argument(EOWNER_CANNOT_BE_MULTISIG_ACCOUNT_ITSELF)); let (found, _) = vector::index_of(&distinct_owners, &owner); assert!(!found, error::invalid_argument(EDUPLICATE_OWNER)); vector::push_back(&mut distinct_owners, owner); });}
assert_is_owner_internal
fun assert_is_owner_internal(owner: &signer, multisig_account: &multisig_account::MultisigAccount)
Implementation
inline fun assert_is_owner_internal(owner: &signer, multisig_account: &MultisigAccount) { assert!( vector::contains(&multisig_account.owners, &address_of(owner)), error::permission_denied(ENOT_OWNER), );}
assert_is_owner
fun assert_is_owner(owner: &signer, multisig_account: address)
Implementation
inline fun assert_is_owner(owner: &signer, multisig_account: address) acquires MultisigAccount { let multisig_account_resource = borrow_global<MultisigAccount>(multisig_account); assert_is_owner_internal(owner, multisig_account_resource);}
num_approvals_and_rejections_internal
fun num_approvals_and_rejections_internal(owners: &vector<address>, transaction: &multisig_account::MultisigTransaction): (u64, u64)
Implementation
inline fun num_approvals_and_rejections_internal(owners: &vector<address>, transaction: &MultisigTransaction): (u64, u64) { let num_approvals = 0; let num_rejections = 0;
let votes = &transaction.votes; vector::for_each_ref(owners, |owner| { if (simple_map::contains_key(votes, owner)) { if (*simple_map::borrow(votes, owner)) { num_approvals = num_approvals + 1; } else { num_rejections = num_rejections + 1; }; } });
(num_approvals, num_rejections)}
num_approvals_and_rejections
fun num_approvals_and_rejections(multisig_account: address, sequence_number: u64): (u64, u64)
Implementation
inline fun num_approvals_and_rejections(multisig_account: address, sequence_number: u64): (u64, u64) acquires MultisigAccount { let multisig_account_resource = borrow_global<MultisigAccount>(multisig_account); let transaction = table::borrow(&multisig_account_resource.transactions, sequence_number); num_approvals_and_rejections_internal(&multisig_account_resource.owners, transaction)}
has_voted_for_approval
fun has_voted_for_approval(multisig_account: address, sequence_number: u64, owner: address): bool
Implementation
inline fun has_voted_for_approval(multisig_account: address, sequence_number: u64, owner: address): bool acquires MultisigAccount { let (voted, vote) = vote(multisig_account, sequence_number, owner); voted && vote}
has_voted_for_rejection
fun has_voted_for_rejection(multisig_account: address, sequence_number: u64, owner: address): bool
Implementation
inline fun has_voted_for_rejection(multisig_account: address, sequence_number: u64, owner: address): bool acquires MultisigAccount { let (voted, vote) = vote(multisig_account, sequence_number, owner); voted && !vote}
assert_multisig_account_exists
fun assert_multisig_account_exists(multisig_account: address)
Implementation
inline fun assert_multisig_account_exists(multisig_account: address) { assert!(exists<MultisigAccount>(multisig_account), error::invalid_state(EACCOUNT_NOT_MULTISIG));}
assert_valid_sequence_number
fun assert_valid_sequence_number(multisig_account: address, sequence_number: u64)
Implementation
inline fun assert_valid_sequence_number(multisig_account: address, sequence_number: u64) acquires MultisigAccount { let multisig_account_resource = borrow_global<MultisigAccount>(multisig_account); assert!( sequence_number > 0 && sequence_number < multisig_account_resource.next_sequence_number, error::invalid_argument(EINVALID_SEQUENCE_NUMBER), );}
assert_transaction_exists
fun assert_transaction_exists(multisig_account: address, sequence_number: u64)
Implementation
inline fun assert_transaction_exists(multisig_account: address, sequence_number: u64) acquires MultisigAccount { let multisig_account_resource = borrow_global<MultisigAccount>(multisig_account); assert!( table::contains(&multisig_account_resource.transactions, sequence_number), error::not_found(ETRANSACTION_NOT_FOUND), );}
update_owner_schema
Add new owners, remove owners to remove, update signatures required.
fun update_owner_schema(multisig_address: address, new_owners: vector<address>, owners_to_remove: vector<address>, optional_new_num_signatures_required: option::Option<u64>)
Implementation
fun update_owner_schema( multisig_address: address, new_owners: vector<address>, owners_to_remove: vector<address>, optional_new_num_signatures_required: Option<u64>,) acquires MultisigAccount { assert_multisig_account_exists(multisig_address); let multisig_account_ref_mut = borrow_global_mut<MultisigAccount>(multisig_address); // Verify no overlap between new owners and owners to remove. vector::for_each_ref(&new_owners, |new_owner_ref| { assert!( !vector::contains(&owners_to_remove, new_owner_ref), error::invalid_argument(EOWNERS_TO_REMOVE_NEW_OWNERS_OVERLAP) ) }); // If new owners provided, try to add them and emit an event. if (vector::length(&new_owners) > 0) { vector::append(&mut multisig_account_ref_mut.owners, new_owners); validate_owners( &multisig_account_ref_mut.owners, multisig_address ); if (std::features::module_event_migration_enabled()) { emit(AddOwners { multisig_account: multisig_address, owners_added: new_owners }); } else { emit_event( &mut multisig_account_ref_mut.add_owners_events, AddOwnersEvent { owners_added: new_owners } ); }; }; // If owners to remove provided, try to remove them. if (vector::length(&owners_to_remove) > 0) { let owners_ref_mut = &mut multisig_account_ref_mut.owners; let owners_removed = vector[]; vector::for_each_ref(&owners_to_remove, |owner_to_remove_ref| { let (found, index) = vector::index_of(owners_ref_mut, owner_to_remove_ref); if (found) { vector::push_back( &mut owners_removed, vector::swap_remove(owners_ref_mut, index) ); } }); // Only emit event if owner(s) actually removed. if (vector::length(&owners_removed) > 0) { if (std::features::module_event_migration_enabled()) { emit( RemoveOwners { multisig_account: multisig_address, owners_removed } ); } else { emit_event( &mut multisig_account_ref_mut.remove_owners_events, RemoveOwnersEvent { owners_removed } ); }; } }; // If new signature count provided, try to update count. if (option::is_some(&optional_new_num_signatures_required)) { let new_num_signatures_required = option::extract(&mut optional_new_num_signatures_required); assert!( new_num_signatures_required > 0, error::invalid_argument(EINVALID_SIGNATURES_REQUIRED) ); let old_num_signatures_required = multisig_account_ref_mut.num_signatures_required; // Only apply update and emit event if a change indicated. if (new_num_signatures_required != old_num_signatures_required) { multisig_account_ref_mut.num_signatures_required = new_num_signatures_required; if (std::features::module_event_migration_enabled()) { emit( UpdateSignaturesRequired { multisig_account: multisig_address, old_num_signatures_required, new_num_signatures_required, } ); } else { emit_event( &mut multisig_account_ref_mut.update_signature_required_events, UpdateSignaturesRequiredEvent { old_num_signatures_required, new_num_signatures_required, } ); } } }; // Verify number of owners. let num_owners = vector::length(&multisig_account_ref_mut.owners); assert!( num_owners >= multisig_account_ref_mut.num_signatures_required, error::invalid_state(ENOT_ENOUGH_OWNERS) );}
Specification
High-level Requirements
No. | Requirement | Criticality | Implementation | Enforcement |
---|---|---|---|---|
1 | For every multi-signature account, the range of required signatures should always be in the range of one to the total number of owners. | Critical | While creating a MultisigAccount, the function create_with_owners_internal checks that num_signatures_required is in the span from 1 to total count of owners. | This has been audited. |
2 | The list of owners for a multi-signature account should not contain any duplicate owners, and the multi-signature account itself cannot be listed as one of its owners. | Critical | The function validate_owners validates the owner vector that no duplicate entries exists. | This has been audited. |
3 | The current value of the next sequence number should not be present in the transaction table, until the next sequence number gets increased. | Medium | The add_transaction function increases the next sequence number and only then adds the transaction with the old next sequence number to the transaction table. | This has been audited. |
4 | When the last executed sequence number is smaller than the next sequence number by only one unit, no transactions should exist in the multi-signature account's transactions list. | High | The get_pending_transactions function retrieves pending transactions by iterating through the transactions table, starting from the last_executed_sequence_number + 1 to the next_sequence_number. | Audited that MultisigAccount.transactions is empty when last_executed_sequence_number == next_sequence_number -1 |
5 | The last executed sequence number is always smaller than the next sequence number. | Medium | When creating a new MultisigAccount, the last_executed_sequence_number and next_sequence_number are assigned with 0 and 1 respectively, and from there both these values increase monotonically when a transaction is executed and removed from the table and when new transaction are added respectively. | This has been audited. |
6 | The number of pending transactions should be equal to the difference between the next sequence number and the last executed sequence number. | High | When a transaction is added, next_sequence_number is incremented. And when a transaction is removed after execution, last_executed_sequence_number is incremented. | This has been audited. |
7 | Only transactions with valid sequence number should be fetched. | Medium | Functions such as: 1. get_transaction 2. can_be_executed 3. can_be_rejected 4. vote always validate the given sequence number and only then fetch the associated transaction. | Audited that it aborts if the sequence number is not valid. |
8 | The execution or rejection of a transaction should enforce that the minimum number of required signatures is less or equal to the total number of approvals. | Critical | The functions can_be_executed and can_be_rejected perform validation on the number of votes required for execution or rejection. | Audited that these functions return the correct value. |
9 | The creation of a multi-signature account properly initializes the resources and then it gets published under the corresponding account. | Medium | When creating a MultisigAccount via one of the functions: create_with_existing_account, create_with_existing_account_and_revoke_auth_key, create_with_owners, create, the MultisigAccount data is initialized properly and published to the multisig_account (new or existing). | Audited that the MultisigAccount is initialized properly. |
10 | Creation of a multi-signature account on top of an existing account should revoke auth key and any previous offered capabilities or control. | Critical | The function create_with_existing_account_and_revoke_auth_key, after successfully creating the MultisigAccount, rotates the account to ZeroAuthKey and revokes any offered capabilities of that account. | Audited that the account's auth key and the offered capabilities are revoked. |
11 | Upon the creation of a multi-signature account from a bootstrapping account, the ownership of the resultant account should not pertain to the bootstrapping account. | High | In create_with_owners_then_remove_bootstrapper function after successful creation of the account the bootstrapping account is removed from the owner vector of the account. | Audited that the bootstrapping account is not in the owners list. |
12 | Performing any changes on the list of owners such as adding new owners, removing owners, swapping owners should ensure that the number of required signature, for the multi-signature account remains valid. | Critical | The following function as used to modify the owners list and the required signature of the account: add_owner, add_owners, add_owners_and_update_signatures_required, remove_owner, remove_owners, swap_owner, swap_owners, swap_owners_and_update_signatures_required, update_signatures_required. All of these functions use update_owner_schema function to process these changes, the function validates the owner list while adding and verifies that the account has enough required signatures and updates the owner's schema. | Audited that the owners are added successfully. (add_owner, add_owners, add_owners_and_update_signatures_required, swap_owner, swap_owners, swap_owners_and_update_signatures_required, update_owner_schema) Audited that the owners are removed successfully. (remove_owner, remove_owners, swap_owner, swap_owners, swap_owners_and_update_signatures_required, update_owner_schema) Audited that the num_signatures_required is updated successfully. (add_owners_and_update_signatures_required, swap_owners_and_update_signatures_required, update_signatures_required, update_owner_schema) |
13 | The creation of a transaction should be limited to an account owner, which should be automatically considered a voter; additionally, the account's sequence should increase monotonically. | Critical | The following functions can only be called by the owners of the account and create a transaction and uses add_transaction function to gives approval on behalf of the creator and increments the next_sequence_number and finally adds the transaction to the MultsigAccount: create_transaction_with_hash, create_transaction. | Audited it aborts if the caller is not in the owner's list of the account. (create_transaction_with_hash, create_transaction) Audited that the transaction is successfully stored in the MultisigAccount.(create_transaction_with_hash, create_transaction, add_transaction) Audited that the creators voted to approve the transaction. (create_transaction_with_hash, create_transaction, add_transaction) Audited that the next_sequence_number increases monotonically. (create_transaction_with_hash, create_transaction, add_transaction) |
14 | Only owners are allowed to vote for a valid transaction. | Critical | Any owner of the MultisigAccount can either approve (approve_transaction) or reject (reject_transaction) a transaction. Both these functions use a generic function to vote for the transaction which validates the caller and the transaction id and adds/updates the vote. | Audited that it aborts if the caller is not in the owner's list (approve_transaction, reject_transaction, vote_transaction, assert_is_owner). Audited that it aborts if the transaction with the given sequence number doesn't exist in the account (approve_transaction, reject_transaction, vote_transaction). Audited that the vote is recorded as intended. |
15 | Only owners are allowed to execute a valid transaction, if the number of approvals meets the k-of-n criteria, finally the executed transaction should be removed. | Critical | Functions execute_rejected_transaction and validate_multisig_transaction can only be called by the owner which validates the transaction and based on the number of approvals and rejections it proceeds to execute the transactions. For rejected transaction, the transactions are immediately removed from the MultisigAccount via remove_executed_transaction. VM validates the transaction via validate_multisig_transaction and cleans up the transaction via successful_transaction_execution_cleanup and failed_transaction_execution_cleanup. | Audited that it aborts if the caller is not in the owner's list (execute_rejected_transaction, validate_multisig_transaction). Audited that it aborts if the transaction with the given sequence number doesn't exist in the account (execute_rejected_transaction, validate_multisig_transaction). Audited that it aborts if the votes (approvals or rejections) are less than num_signatures_required (execute_rejected_transaction, validate_multisig_transaction). Audited that the transaction is removed from the MultisigAccount (execute_rejected_transaction, remove_executed_transaction, successful_transaction_execution_cleanup, failed_transaction_execution_cleanup). |
16 | Removing an executed transaction from the transactions list should increase the last sequence number monotonically. | High | When transactions are removed via remove_executed_transaction (maybe called by VM cleanup or execute_rejected_transaction), the last_executed_sequence_number increases by 1. | Audited that last_executed_sequence_number is incremented. |
17 | The voting and transaction creation operations should only be available if a multi-signature account exists. | Low | The function assert_multisig_account_exists validates the existence of MultisigAccount under the account. | Audited that it aborts if the MultisigAccount doesn't exist on the account. |
Module-level Specification
metadata
#[view]public fun metadata(multisig_account: address): simple_map::SimpleMap<string::String, vector<u8>>
aborts_if !exists<MultisigAccount>(multisig_account);ensures result == global<MultisigAccount>(multisig_account).metadata;
num_signatures_required
#[view]public fun num_signatures_required(multisig_account: address): u64
aborts_if !exists<MultisigAccount>(multisig_account);ensures result == global<MultisigAccount>(multisig_account).num_signatures_required;
owners
#[view]public fun owners(multisig_account: address): vector<address>
aborts_if !exists<MultisigAccount>(multisig_account);ensures result == global<MultisigAccount>(multisig_account).owners;
get_transaction
#[view]public fun get_transaction(multisig_account: address, sequence_number: u64): multisig_account::MultisigTransaction
let multisig_account_resource = global<MultisigAccount>(multisig_account);aborts_if !exists<MultisigAccount>(multisig_account);aborts_if sequence_number == 0 || sequence_number >= multisig_account_resource.next_sequence_number;aborts_if !table::spec_contains(multisig_account_resource.transactions, sequence_number);ensures result == table::spec_get(multisig_account_resource.transactions, sequence_number);
get_next_transaction_payload
#[view]public fun get_next_transaction_payload(multisig_account: address, provided_payload: vector<u8>): vector<u8>
let multisig_account_resource = global<MultisigAccount>(multisig_account);let sequence_number = multisig_account_resource.last_executed_sequence_number + 1;let transaction = table::spec_get(multisig_account_resource.transactions, sequence_number);aborts_if !exists<MultisigAccount>(multisig_account);aborts_if multisig_account_resource.last_executed_sequence_number + 1 > MAX_U64;aborts_if !table::spec_contains(multisig_account_resource.transactions, sequence_number);ensures option::spec_is_none(transaction.payload) ==> result == provided_payload;
get_next_multisig_account_address
#[view]public fun get_next_multisig_account_address(creator: address): address
let owner_nonce = global<account::Account>(creator).sequence_number;
last_resolved_sequence_number
#[view]public fun last_resolved_sequence_number(multisig_account: address): u64
let multisig_account_resource = global<MultisigAccount>(multisig_account);aborts_if !exists<MultisigAccount>(multisig_account);ensures result == multisig_account_resource.last_executed_sequence_number;
next_sequence_number
#[view]public fun next_sequence_number(multisig_account: address): u64
let multisig_account_resource = global<MultisigAccount>(multisig_account);aborts_if !exists<MultisigAccount>(multisig_account);ensures result == multisig_account_resource.next_sequence_number;
vote
#[view]public fun vote(multisig_account: address, sequence_number: u64, owner: address): (bool, bool)
let multisig_account_resource = global<MultisigAccount>(multisig_account);aborts_if !exists<MultisigAccount>(multisig_account);aborts_if sequence_number == 0 || sequence_number >= multisig_account_resource.next_sequence_number;aborts_if !table::spec_contains(multisig_account_resource.transactions, sequence_number);let transaction = table::spec_get(multisig_account_resource.transactions, sequence_number);let votes = transaction.votes;let voted = simple_map::spec_contains_key(votes, owner);let vote = voted && simple_map::spec_get(votes, owner);ensures result_1 == voted;ensures result_2 == vote;