diff --git a/aptos-move/framework/aptos-framework/doc/multisig_account.md b/aptos-move/framework/aptos-framework/doc/multisig_account.md
new file mode 100644
index 00000000000000..e2e70c099d35e6
--- /dev/null
+++ b/aptos-move/framework/aptos-framework/doc/multisig_account.md
@@ -0,0 +1,1688 @@
+
+
+
+# Module `0x1::multisig_account`
+
+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 acount nonce).
+
+The flow is like below:
+1. 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.
+2. 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.
+3. To create a new transaction, an owner can call create_transaction with the transaction payload: specified module
+(address + name), the name of the function to call, and argument values. This will store the full transaction
+payload on chain, which adds decentralization (censorship is not possible) 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 (module + function + args). Later execution will
+be verified using the hash. Only owners can create transactions and a transaction id (incremeting id) will be
+assigned.
+4. To approve or reject a transaction, other owners can call approve() or reject() with the transaction id.
+5. 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.
+6. If there are enough rejections, any owner can remove the transaction by calling remove().
+
+
+- [Resource `MultisigAccount`](#0x1_multisig_account_MultisigAccount)
+- [Struct `MultisigTransaction`](#0x1_multisig_account_MultisigTransaction)
+- [Struct `TransactionPayload`](#0x1_multisig_account_TransactionPayload)
+- [Struct `PayloadHash`](#0x1_multisig_account_PayloadHash)
+- [Struct `AddOwnersEvent`](#0x1_multisig_account_AddOwnersEvent)
+- [Struct `RemoveOwnersEvent`](#0x1_multisig_account_RemoveOwnersEvent)
+- [Struct `UpdateSignaturesRequiredEvent`](#0x1_multisig_account_UpdateSignaturesRequiredEvent)
+- [Struct `CreateTransactionEvent`](#0x1_multisig_account_CreateTransactionEvent)
+- [Struct `ApproveTransactionEvent`](#0x1_multisig_account_ApproveTransactionEvent)
+- [Struct `RejectTransactionEvent`](#0x1_multisig_account_RejectTransactionEvent)
+- [Struct `RemoveTransactionEvent`](#0x1_multisig_account_RemoveTransactionEvent)
+- [Struct `ExecuteTransactionEvent`](#0x1_multisig_account_ExecuteTransactionEvent)
+- [Constants](#@Constants_0)
+- [Function `signatures_required`](#0x1_multisig_account_signatures_required)
+- [Function `owners`](#0x1_multisig_account_owners)
+- [Function `get_transaction`](#0x1_multisig_account_get_transaction)
+- [Function `can_be_executed`](#0x1_multisig_account_can_be_executed)
+- [Function `can_be_removed`](#0x1_multisig_account_can_be_removed)
+- [Function `get_possible_multisig_account_address`](#0x1_multisig_account_get_possible_multisig_account_address)
+- [Function `create`](#0x1_multisig_account_create)
+- [Function `create_with_owners`](#0x1_multisig_account_create_with_owners)
+- [Function `add_owners`](#0x1_multisig_account_add_owners)
+- [Function `remove_owners`](#0x1_multisig_account_remove_owners)
+- [Function `update_signatures_required`](#0x1_multisig_account_update_signatures_required)
+- [Function `create_transaction`](#0x1_multisig_account_create_transaction)
+- [Function `create_transaction_with_hash`](#0x1_multisig_account_create_transaction_with_hash)
+- [Function `approve_transaction`](#0x1_multisig_account_approve_transaction)
+- [Function `reject_transaction`](#0x1_multisig_account_reject_transaction)
+- [Function `vote_transanction`](#0x1_multisig_account_vote_transanction)
+- [Function `remove_transaction`](#0x1_multisig_account_remove_transaction)
+- [Function `execute_transaction`](#0x1_multisig_account_execute_transaction)
+- [Function `add_transaction`](#0x1_multisig_account_add_transaction)
+- [Function `create_multisig_account`](#0x1_multisig_account_create_multisig_account)
+- [Function `create_multisig_account_seed`](#0x1_multisig_account_create_multisig_account_seed)
+- [Function `validate_owners`](#0x1_multisig_account_validate_owners)
+- [Function `assert_is_owner`](#0x1_multisig_account_assert_is_owner)
+- [Function `assert_multisig_account_exists`](#0x1_multisig_account_assert_multisig_account_exists)
+
+
+
use 0x1::account;
+use 0x1::aptos_coin;
+use 0x1::coin;
+use 0x1::error;
+use 0x1::event;
+use 0x1::hash;
+use 0x1::option;
+use 0x1::signer;
+use 0x1::simple_map;
+use 0x1::string;
+use 0x1::table;
+use 0x1::vector;
+
+
+
+
+
+
+## Resource `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>
+
+-
+
+
+-
+
signatures_required: u64
+
+-
+
+
+-
+
transactions: table::Table<u64, multisig_account::MultisigTransaction>
+
+-
+
+
+-
+
last_transaction_id: u64
+
+-
+
+
+-
+
next_transaction_id: u64
+
+-
+
+
+-
+
signer_cap: account::SignerCapability
+
+-
+
+
+-
+
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>
+
+-
+
+
+-
+
approve_transaction_events: event::EventHandle<multisig_account::ApproveTransactionEvent>
+
+-
+
+
+-
+
reject_transaction_events: event::EventHandle<multisig_account::RejectTransactionEvent>
+
+-
+
+
+-
+
execute_transaction_events: event::EventHandle<multisig_account::ExecuteTransactionEvent>
+
+-
+
+
+-
+
remove_transaction_events: event::EventHandle<multisig_account::RemoveTransactionEvent>
+
+-
+
+
+
+
+
+
+
+
+
+## Struct `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<multisig_account::TransactionPayload>
+
+-
+
+
+-
+
payload_hash: option::Option<multisig_account::PayloadHash>
+
+-
+
+
+-
+
approvals: simple_map::SimpleMap<address, bool>
+
+-
+
+
+-
+
rejections: simple_map::SimpleMap<address, bool>
+
+-
+
+
+-
+
creator: address
+
+-
+
+
+
+
+
+
+
+
+
+## Struct `TransactionPayload`
+
+The payload of the transaction to store on chain.
+
+
+struct TransactionPayload has copy, drop, store
+
+
+
+
+
+Fields
+
+
+
+-
+
target_function: string::String
+
+-
+
+
+-
+
args: vector<u8>
+
+-
+
+
+
+
+
+
+
+
+
+## Struct `PayloadHash`
+
+The hash of the multisig transaction payload.
+
+
+struct PayloadHash has copy, drop, store
+
+
+
+
+
+Fields
+
+
+
+-
+
function_hash: vector<u8>
+
+-
+
+
+-
+
args_hash: vector<u8>
+
+-
+
+
+
+
+
+
+
+
+
+## Struct `AddOwnersEvent`
+
+Event emitted when new owners are added to the multisig account.
+
+
+struct AddOwnersEvent has drop, store
+
+
+
+
+
+Fields
+
+
+
+-
+
owners_added: vector<address>
+
+-
+
+
+
+
+
+
+
+
+
+## Struct `RemoveOwnersEvent`
+
+Event emitted when new owners are removed from the multisig account.
+
+
+struct RemoveOwnersEvent has drop, store
+
+
+
+
+
+Fields
+
+
+
+-
+
owners_removed: vector<address>
+
+-
+
+
+
+
+
+
+
+
+
+## Struct `UpdateSignaturesRequiredEvent`
+
+Event emitted when the number of signatures required is updated.
+
+
+struct UpdateSignaturesRequiredEvent has drop, store
+
+
+
+
+
+Fields
+
+
+
+-
+
old_signatures_required: u64
+
+-
+
+
+-
+
new_signatures_required: u64
+
+-
+
+
+
+
+
+
+
+
+
+## Struct `CreateTransactionEvent`
+
+Event emitted when a transaction is created.
+
+
+struct CreateTransactionEvent has drop, store
+
+
+
+
+
+Fields
+
+
+
+-
+
transaction_id: u64
+
+-
+
+
+-
+
transaction: multisig_account::MultisigTransaction
+
+-
+
+
+-
+
creator: address
+
+-
+
+
+
+
+
+
+
+
+
+## Struct `ApproveTransactionEvent`
+
+Event emitted when an owner approves a transaction.
+
+
+struct ApproveTransactionEvent has drop, store
+
+
+
+
+
+Fields
+
+
+
+-
+
transaction_id: u64
+
+-
+
+
+-
+
owner: address
+
+-
+
+
+-
+
num_approvals: u64
+
+-
+
+
+
+
+
+
+
+
+
+## Struct `RejectTransactionEvent`
+
+Event emitted when an owner rejects a transaction.
+
+
+struct RejectTransactionEvent has drop, store
+
+
+
+
+
+Fields
+
+
+
+-
+
transaction_id: u64
+
+-
+
+
+-
+
owner: address
+
+-
+
+
+-
+
num_rejections: u64
+
+-
+
+
+
+
+
+
+
+
+
+## Struct `RemoveTransactionEvent`
+
+Event emitted when a transaction is officially removed because the number of rejections have reached the
+number of signatures required.
+
+
+struct RemoveTransactionEvent has drop, store
+
+
+
+
+
+Fields
+
+
+
+-
+
transaction_id: u64
+
+-
+
+
+-
+
num_rejections: u64
+
+-
+
+
+-
+
executor: address
+
+-
+
+
+
+
+
+
+
+
+
+## Struct `ExecuteTransactionEvent`
+
+Event emitted when a transaction is executed.
+
+
+struct ExecuteTransactionEvent has drop, store
+
+
+
+
+
+Fields
+
+
+
+-
+
transaction_id: u64
+
+-
+
+
+-
+
transaction_payload: multisig_account::TransactionPayload
+
+-
+
+
+-
+
num_approvals: u64
+
+-
+
+
+-
+
executor: address
+
+-
+
+
+
+
+
+
+
+
+
+## Constants
+
+
+
+
+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 SALT: 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 = 2;
+
+
+
+
+
+
+Provided arguments do not match the hash stored in the on-chain transaction.
+
+
+const EARGUMENTS_DOES_NOT_MATCH_HASH: u64 = 10;
+
+
+
+
+
+
+Owner list cannot contain the same address more than once.
+
+
+const EDUPLICATE_OWNER: u64 = 1;
+
+
+
+
+
+
+Function hash cannot be empty.
+
+
+const EFUNCTION_HASH_CANNOT_BE_EMPTY: u64 = 6;
+
+
+
+
+
+
+Number of signatures required must be more than zero and at most the total number of owners.
+
+
+const EINVALID_SIGNATURES_REQUIRED: u64 = 14;
+
+
+
+
+
+
+Transaction has not received enough approvals to be executed.
+
+
+const ENOT_ENOUGH_APPROVALS: u64 = 11;
+
+
+
+
+
+
+Multisig account must have at least one owner.
+
+
+const ENOT_ENOUGH_OWNERS: u64 = 5;
+
+
+
+
+
+
+Transaction has not received enough rejections to be removed.
+
+
+const ENOT_ENOUGH_REJECTIONS: u64 = 13;
+
+
+
+
+
+
+Account executing this operation is not an owner of the multisig account.
+
+
+const ENOT_OWNER: u64 = 3;
+
+
+
+
+
+
+Cannot execute the specified transaction simply via transaction_id as the full payload is not stored on chain.
+
+
+const EPAYLOAD_NOT_STORED: u64 = 8;
+
+
+
+
+
+
+Target function cannot be empty.
+
+
+const ETARGET_FUNCTION_CANNOT_BE_EMPTY: u64 = 4;
+
+
+
+
+
+
+Provided target function does not match the hash stored in the on-chain transaction.
+
+
+const ETARGET_FUNCTION_DOES_NOT_MATCH_HASH: u64 = 9;
+
+
+
+
+
+
+Transactions have to be approved or rejected in creation order.
+
+
+const ETRANSACTION_CANNOT_BE_EXECUTED_OUT_OF_ORDER: u64 = 12;
+
+
+
+
+
+
+Transaction with specified id cannot be found. It either has not been created or has already been executed.
+
+
+const ETRANSACTION_NOT_FOUND: u64 = 7;
+
+
+
+
+
+
+## Function `signatures_required`
+
+
+
+public fun signatures_required(mutlisig_account: address): u64
+
+
+
+
+
+Implementation
+
+
+public fun signatures_required(mutlisig_account: address): u64 acquires MultisigAccount {
+ borrow_global<MultisigAccount>(mutlisig_account).signatures_required
+}
+
+
+
+
+
+
+
+
+## Function `owners`
+
+
+
+public fun owners(mutlisig_account: address): vector<address>
+
+
+
+
+
+Implementation
+
+
+public fun owners(mutlisig_account: address): vector<address> acquires MultisigAccount {
+ borrow_global<MultisigAccount>(mutlisig_account).owners
+}
+
+
+
+
+
+
+
+
+## Function `get_transaction`
+
+
+
+public fun get_transaction(mutlisig_account: address, transaction_id: u64): multisig_account::MultisigTransaction
+
+
+
+
+
+Implementation
+
+
+public fun get_transaction(
+ mutlisig_account: address,
+ transaction_id: u64,
+): MultisigTransaction acquires MultisigAccount {
+ *table::borrow(&borrow_global<MultisigAccount>(mutlisig_account).transactions, transaction_id)
+}
+
+
+
+
+
+
+
+
+## Function `can_be_executed`
+
+
+
+public fun can_be_executed(multisig_account: address, transaction_id: u64): bool
+
+
+
+
+
+Implementation
+
+
+public fun can_be_executed(
+ multisig_account: address, transaction_id: u64): bool acquires MultisigAccount {
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
+ let transaction = table::borrow(&mut multisig_account_resource.transactions, transaction_id);
+ transaction_id == multisig_account_resource.last_transaction_id + 1 &&
+ simple_map::length(&transaction.approvals) >= multisig_account_resource.signatures_required
+}
+
+
+
+
+
+
+
+
+## Function `can_be_removed`
+
+
+
+public fun can_be_removed(multisig_account: address, transaction_id: u64): bool
+
+
+
+
+
+Implementation
+
+
+public fun can_be_removed(
+ multisig_account: address, transaction_id: u64): bool acquires MultisigAccount {
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
+ let transaction = table::borrow(&mut multisig_account_resource.transactions, transaction_id);
+ transaction_id == multisig_account_resource.last_transaction_id + 1 &&
+ simple_map::length(&transaction.rejections) >= multisig_account_resource.signatures_required
+}
+
+
+
+
+
+
+
+
+## Function `get_possible_multisig_account_address`
+
+
+
+public fun get_possible_multisig_account_address(creator: address, seed: vector<u8>): address
+
+
+
+
+
+Implementation
+
+
+public fun get_possible_multisig_account_address(creator: address, seed: vector<u8>): address {
+ create_resource_address(&creator, create_multisig_account_seed(seed))
+}
+
+
+
+
+
+
+
+
+## Function `create`
+
+Creates a new multisig account and add the signer as a single owner.
+The seed is optional and can be used to create multiple multisig accounts from the same owner account. A good
+seed to use is the number of multisig accounts created so far with the same owner account.
+
+
+public entry fun create(owner: &signer, signatures_required: u64, seed: vector<u8>)
+
+
+
+
+
+Implementation
+
+
+public entry fun create(owner: &signer, signatures_required: u64, seed: vector<u8>) {
+ create_with_owners(owner, vector[], signatures_required, seed);
+}
+
+
+
+
+
+
+
+
+## Function `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 signatures_require 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>, signatures_required: u64, seed: vector<u8>)
+
+
+
+
+
+Implementation
+
+
+public entry fun create_with_owners(owner: &signer, additional_owners: vector<address>, signatures_required: u64, seed: vector<u8>) {
+ let owner_address = address_of(owner);
+ vector::push_back(&mut additional_owners, owner_address);
+ validate_owners(&additional_owners);
+ assert!(
+ signatures_required > 0 && signatures_required <= vector::length(&additional_owners),
+ error::invalid_argument(EINVALID_SIGNATURES_REQUIRED),
+ );
+
+ let (multisig_signer, multisig_signer_cap) = create_multisig_account(owner, seed);
+ move_to(&multisig_signer, MultisigAccount {
+ owners: additional_owners,
+ signatures_required,
+ transactions: table::new<u64, MultisigTransaction>(),
+ // First transaction will start at id 1 instead of 0.
+ last_transaction_id: 0,
+ next_transaction_id: 1,
+ signer_cap: multisig_signer_cap,
+ add_owners_events: new_event_handle<AddOwnersEvent>(&multisig_signer),
+ remove_owners_events: new_event_handle<RemoveOwnersEvent>(&multisig_signer),
+ update_signature_required_events: new_event_handle<UpdateSignaturesRequiredEvent>(&multisig_signer),
+ create_transaction_events: new_event_handle<CreateTransactionEvent>(&multisig_signer),
+ approve_transaction_events: new_event_handle<ApproveTransactionEvent>(&multisig_signer),
+ reject_transaction_events: new_event_handle<RejectTransactionEvent>(&multisig_signer),
+ execute_transaction_events: new_event_handle<ExecuteTransactionEvent>(&multisig_signer),
+ remove_transaction_events: new_event_handle<RemoveTransactionEvent>(&multisig_signer),
+ });
+}
+
+
+
+
+
+
+
+
+## Function `add_owners`
+
+Add new owners to the multisig account. This can only be invoked by the multisig account itself, through the
+proposal flow.
+
+
+public entry fun add_owners(multisig_account: &signer, new_owners: vector<address>)
+
+
+
+
+
+Implementation
+
+
+public entry fun add_owners(multisig_account: &signer, new_owners: vector<address>) acquires MultisigAccount {
+ // Short circuit if new owners list is empty.
+ // This avoids emitting an event if no changes happen, which is confusing to off-chain components.
+ if (vector::length(&new_owners) == 0) {
+ return
+ };
+
+ let multisig_address = address_of(multisig_account);
+ assert_multisig_account_exists(multisig_address);
+ let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_address);
+
+ vector::append(&mut multisig_account_resource.owners, new_owners);
+ validate_owners(&multisig_account_resource.owners);
+ emit_event(&mut multisig_account_resource.add_owners_events, AddOwnersEvent {
+ owners_added: new_owners,
+ });
+}
+
+
+
+
+
+
+
+
+## Function `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.
+
+
+public entry fun remove_owners(multisig_account: &signer, owners_to_remove: vector<address>)
+
+
+
+
+
+Implementation
+
+
+public entry fun remove_owners(
+ multisig_account: &signer, owners_to_remove: vector<address>) acquires MultisigAccount {
+ // Short circuit if the list of owners to remove is empty.
+ // This avoids emitting an event if no changes happen, which is confusing to off-chain components.
+ if (vector::length(&owners_to_remove) == 0) {
+ return
+ };
+
+ let multisig_address = address_of(multisig_account);
+ assert_multisig_account_exists(multisig_address);
+ let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_address);
+
+ let i = 0;
+ let len = vector::length(&owners_to_remove);
+ let owners = &mut multisig_account_resource.owners;
+ let owners_removed = vector::empty<address>();
+ while (i < len) {
+ let owner_to_remove = *vector::borrow(&owners_to_remove, i);
+ let (found, index) = vector::index_of(owners, &owner_to_remove);
+ // Only remove an owner if they're present in the owners list.
+ if (found) {
+ vector::push_back(&mut owners_removed, owner_to_remove);
+ vector::swap_remove(owners, index);
+ };
+ i = i + 1;
+ };
+
+ // Make sure there's still at least 1 owner.
+ assert!(vector::length(owners) > 0, error::invalid_state(ENOT_ENOUGH_OWNERS));
+
+ emit_event(&mut multisig_account_resource.remove_owners_events, RemoveOwnersEvent { owners_removed });
+}
+
+
+
+
+
+
+
+
+## Function `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.
+
+
+public entry fun update_signatures_required(multisig_account: &signer, new_signatures_required: u64)
+
+
+
+
+
+Implementation
+
+
+public entry fun update_signatures_required(
+ multisig_account: &signer, new_signatures_required: u64) acquires MultisigAccount {
+ let multisig_address = address_of(multisig_account);
+ assert_multisig_account_exists(multisig_address);
+ let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_address);
+ let num_owners = vector::length(&multisig_account_resource.owners);
+ assert!(
+ new_signatures_required > 0 && new_signatures_required <= num_owners,
+ error::invalid_argument(EINVALID_SIGNATURES_REQUIRED),
+ );
+
+ // Short-circuit if the new number of signatures required is the same as before.
+ // This avoids emitting an event.
+ if (multisig_account_resource.signatures_required == new_signatures_required) {
+ return
+ };
+ let old_signatures_required = multisig_account_resource.signatures_required;
+ multisig_account_resource.signatures_required = new_signatures_required;
+ emit_event(
+ &mut multisig_account_resource.update_signature_required_events,
+ UpdateSignaturesRequiredEvent {
+ old_signatures_required,
+ new_signatures_required,
+ }
+ );
+}
+
+
+
+
+
+
+
+
+## Function `create_transaction`
+
+Create a multisig transaction, which will have one approval initially (from the creator).
+
+@param target_function The target function to call such as 0x123::module_to_call::function_to_call.
+@param args Vector of BCS-encoded argument values to invoke the target function with.
+
+
+public entry fun create_transaction(owner: &signer, multisig_account: address, target_function: string::String, args: vector<u8>)
+
+
+
+
+
+Implementation
+
+
+public entry fun create_transaction(
+ owner: &signer,
+ multisig_account: address,
+ target_function: String,
+ args: vector<u8>,
+) acquires MultisigAccount {
+ assert!(string::length(&target_function) > 0, error::invalid_argument(ETARGET_FUNCTION_CANNOT_BE_EMPTY));
+
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
+ assert_is_owner(owner, multisig_account_resource);
+
+ let creator = address_of(owner);
+ let transaction = MultisigTransaction {
+ payload: option::some(TransactionPayload { target_function, args }),
+ payload_hash: option::none<PayloadHash>(),
+ approvals: simple_map::create<address, bool>(),
+ rejections: simple_map::create<address, bool>(),
+ creator,
+ };
+ add_transaction(creator, multisig_account_resource, transaction);
+}
+
+
+
+
+
+
+
+
+## Function `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.
+
+@param function_hash The sha-256 hash of the function to invoke, e.g. 0x123::module_to_call::function_to_call.
+@param args_hash The sha-256 hash of the function arguments - a concatenated vector of the bcs-encoded
+function arguments.
+
+
+public entry fun create_transaction_with_hash(owner: &signer, multisig_account: address, function_hash: vector<u8>, args_hash: vector<u8>)
+
+
+
+
+
+Implementation
+
+
+public entry fun create_transaction_with_hash(
+ owner: &signer,
+ multisig_account: address,
+ function_hash: vector<u8>,
+ args_hash: vector<u8>,
+) acquires MultisigAccount {
+ assert!(vector::length(&function_hash) > 0, error::invalid_argument(EFUNCTION_HASH_CANNOT_BE_EMPTY));
+
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
+ assert_is_owner(owner, multisig_account_resource);
+
+ let creator = address_of(owner);
+ let transaction = MultisigTransaction {
+ payload: option::none<TransactionPayload>(),
+ payload_hash: option::some(PayloadHash { function_hash, args_hash }),
+ approvals: simple_map::create<address, bool>(),
+ rejections: simple_map::create<address, bool>(),
+ creator,
+ };
+ add_transaction(creator, multisig_account_resource, transaction);
+}
+
+
+
+
+
+
+
+
+## Function `approve_transaction`
+
+Approve a multisig transaction.
+
+
+public entry fun approve_transaction(owner: &signer, multisig_account: address, transaction_id: u64)
+
+
+
+
+
+Implementation
+
+
+public entry fun approve_transaction(
+ owner: &signer, multisig_account: address, transaction_id: u64) acquires MultisigAccount {
+ vote_transanction(owner, multisig_account, transaction_id, true);
+}
+
+
+
+
+
+
+
+
+## Function `reject_transaction`
+
+Reject a multisig transaction.
+
+
+public entry fun reject_transaction(owner: &signer, multisig_account: address, transaction_id: u64)
+
+
+
+
+
+Implementation
+
+
+public entry fun reject_transaction(
+ owner: &signer, multisig_account: address, transaction_id: u64) acquires MultisigAccount {
+ vote_transanction(owner, multisig_account, transaction_id, false);
+}
+
+
+
+
+
+
+
+
+## Function `vote_transanction`
+
+Generic function that can be used to either approve or reject a multisig transaction
+
+
+public entry fun vote_transanction(owner: &signer, multisig_account: address, transaction_id: u64, approved: bool)
+
+
+
+
+
+Implementation
+
+
+public entry fun vote_transanction(
+ owner: &signer, multisig_account: address, transaction_id: u64, approved: bool) acquires MultisigAccount {
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
+ assert_is_owner(owner, multisig_account_resource);
+
+ assert!(
+ table::contains(&multisig_account_resource.transactions, transaction_id),
+ error::not_found(ETRANSACTION_NOT_FOUND),
+ );
+ let transaction = table::borrow_mut(&mut multisig_account_resource.transactions, transaction_id);
+ let owner_addr = address_of(owner);
+ if (approved) {
+ simple_map::add(&mut transaction.approvals, address_of(owner), true);
+ emit_event(
+ &mut multisig_account_resource.approve_transaction_events,
+ ApproveTransactionEvent {
+ owner: owner_addr,
+ transaction_id,
+ num_approvals: simple_map::length(&transaction.approvals),
+ }
+ );
+ } else {
+ simple_map::add(&mut transaction.rejections, address_of(owner), true);
+ emit_event(
+ &mut multisig_account_resource.reject_transaction_events,
+ RejectTransactionEvent {
+ owner: owner_addr,
+ transaction_id,
+ num_rejections: simple_map::length(&transaction.rejections),
+ }
+ );
+ };
+}
+
+
+
+
+
+
+
+
+## Function `remove_transaction`
+
+Remove a transaction that has sufficient owner rejections.
+
+@param transaction_id Id of the transaction to execute.
+
+
+public entry fun remove_transaction(owner: &signer, multisig_account: address, transaction_id: u64)
+
+
+
+
+
+Implementation
+
+
+public entry fun remove_transaction(
+ owner: &signer,
+ multisig_account: address,
+ transaction_id: u64,
+) acquires MultisigAccount {
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
+ assert_is_owner(owner, multisig_account_resource);
+ assert!(
+ table::contains(&multisig_account_resource.transactions, transaction_id),
+ error::not_found(ETRANSACTION_NOT_FOUND),
+ );
+ let transaction = table::remove(&mut multisig_account_resource.transactions, transaction_id);
+ assert!(
+ transaction_id == multisig_account_resource.last_transaction_id + 1,
+ error::invalid_argument(ETRANSACTION_CANNOT_BE_EXECUTED_OUT_OF_ORDER),
+ );
+ assert!(
+ simple_map::length(&transaction.rejections) >= multisig_account_resource.signatures_required,
+ error::invalid_state(ENOT_ENOUGH_REJECTIONS),
+ );
+
+ multisig_account_resource.last_transaction_id = transaction_id;
+ emit_event(
+ &mut multisig_account_resource.remove_transaction_events,
+ RemoveTransactionEvent {
+ transaction_id,
+ num_rejections: simple_map::length(&transaction.rejections),
+ executor: address_of(owner),
+ }
+ );
+}
+
+
+
+
+
+
+
+
+## Function `execute_transaction`
+
+Execute a transaction. This doesn't actually invoke the target function but simply marks the transaction as
+already been executed (by removing it from the transactions table). Actual function invocation is done as part
+executing the MultisigTransaction.
+
+This function is private so no other code can call this beside the VM itself as part of MultisigTransaction.
+
+@param transaction_id Id of the transaction to execute.
+@param target_function Optional and can be empty if the full transaction payload is stored on chain.
+@param args Optional and can be empty if the full transaction payload is stored on chain.
+@return The transaction payload to execute as the multisig account.
+
+
+fun execute_transaction(owner: &signer, multisig_account: address, transaction_id: u64, target_function: string::String, args: vector<u8>): multisig_account::TransactionPayload
+
+
+
+
+
+Implementation
+
+
+fun execute_transaction(
+ owner: &signer,
+ multisig_account: address,
+ transaction_id: u64,
+ target_function: String,
+ args: vector<u8>,
+): TransactionPayload acquires MultisigAccount {
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
+ assert_is_owner(owner, multisig_account_resource);
+ assert!(
+ table::contains(&multisig_account_resource.transactions, transaction_id),
+ error::not_found(ETRANSACTION_NOT_FOUND),
+ );
+ let transaction = table::remove(&mut multisig_account_resource.transactions, transaction_id);
+ assert!(
+ transaction_id == multisig_account_resource.last_transaction_id + 1,
+ error::invalid_argument(ETRANSACTION_CANNOT_BE_EXECUTED_OUT_OF_ORDER),
+ );
+ assert!(
+ simple_map::length(&transaction.approvals) >= multisig_account_resource.signatures_required,
+ error::invalid_state(ENOT_ENOUGH_APPROVALS),
+ );
+
+ let transaction_payload =
+ if (option::is_some(&transaction.payload)) {
+ option::extract(&mut transaction.payload)
+ } else {
+ let payload_hash = option::extract(&mut transaction.payload_hash);
+ assert!(
+ sha3_256(*string::bytes(&target_function)) == payload_hash.function_hash,
+ error::invalid_argument(ETARGET_FUNCTION_DOES_NOT_MATCH_HASH),
+ );
+ assert!(
+ sha3_256(args) == payload_hash.args_hash,
+ error::invalid_argument(EARGUMENTS_DOES_NOT_MATCH_HASH),
+ );
+ TransactionPayload { target_function, args }
+ };
+ multisig_account_resource.last_transaction_id = transaction_id;
+ emit_event(
+ &mut multisig_account_resource.execute_transaction_events,
+ ExecuteTransactionEvent {
+ transaction_id,
+ transaction_payload,
+ num_approvals: simple_map::length(&transaction.approvals),
+ executor: address_of(owner),
+ }
+ );
+
+ transaction_payload
+}
+
+
+
+
+
+
+
+
+## Function `add_transaction`
+
+
+
+fun add_transaction(creator: address, multisig_account: &mut multisig_account::MultisigAccount, transaction: multisig_account::MultisigTransaction)
+
+
+
+
+
+Implementation
+
+
+fun add_transaction(creator: address, multisig_account: &mut MultisigAccount, transaction: MultisigTransaction) {
+ simple_map::add(&mut transaction.approvals, creator, true);
+
+ let transaction_id = multisig_account.next_transaction_id;
+ multisig_account.next_transaction_id = transaction_id + 1;
+ table::add(&mut multisig_account.transactions, transaction_id, transaction);
+ emit_event(
+ &mut multisig_account.create_transaction_events,
+ CreateTransactionEvent { transaction_id, transaction, creator },
+ );
+}
+
+
+
+
+
+
+
+
+## Function `create_multisig_account`
+
+
+
+fun create_multisig_account(owner: &signer, seed: vector<u8>): (signer, account::SignerCapability)
+
+
+
+
+
+Implementation
+
+
+fun create_multisig_account(owner: &signer, seed: vector<u8>): (signer, SignerCapability) {
+ let (multisig_signer, multisig_signer_cap) =
+ account::create_resource_account(owner, create_multisig_account_seed(seed));
+ // 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)
+}
+
+
+
+
+
+
+
+
+## Function `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 staking contract.
+ let multisig_account_seed = vector::empty<u8>();
+ vector::append(&mut multisig_account_seed, SALT);
+ // Add an extra salt given by the owner in case an account with the same address has already been created.
+ vector::append(&mut multisig_account_seed, seed);
+
+ multisig_account_seed
+}
+
+
+
+
+
+
+
+
+## Function `validate_owners`
+
+
+
+fun validate_owners(owners: &vector<address>)
+
+
+
+
+
+Implementation
+
+
+fun validate_owners(owners: &vector<address>) {
+ let distinct_owners = simple_map::create<address, bool>();
+ let i = 0;
+ let len = vector::length(owners);
+ while (i < len) {
+ let owner = *vector::borrow(owners, i);
+ assert!(
+ !simple_map::contains_key(&distinct_owners, &owner),
+ error::invalid_argument(EDUPLICATE_OWNER),
+ );
+ simple_map::add(&mut distinct_owners, owner, true);
+ i = i + 1;
+ }
+}
+
+
+
+
+
+
+
+
+## Function `assert_is_owner`
+
+
+
+fun assert_is_owner(owner: &signer, multisig_account: &multisig_account::MultisigAccount)
+
+
+
+
+
+Implementation
+
+
+fun assert_is_owner(owner: &signer, multisig_account: &MultisigAccount) {
+ assert!(
+ vector::contains(&multisig_account.owners, &address_of(owner)),
+ error::permission_denied(ENOT_OWNER),
+ );
+}
+
+
+
+
+
+
+
+
+## Function `assert_multisig_account_exists`
+
+
+
+fun assert_multisig_account_exists(multisig_account: address)
+
+
+
+
+
+Implementation
+
+
+fun assert_multisig_account_exists(multisig_account: address) {
+ assert!(exists<MultisigAccount>(multisig_account), error::invalid_state(EACCOUNT_NOT_MULTISIG));
+}
+
+
+
+
+
+
+
+[move-book]: https://move-language.github.io/move/introduction.html
diff --git a/aptos-move/framework/aptos-framework/doc/overview.md b/aptos-move/framework/aptos-framework/doc/overview.md
index 28c38eae5c70d0..b1aafca0aa2067 100644
--- a/aptos-move/framework/aptos-framework/doc/overview.md
+++ b/aptos-move/framework/aptos-framework/doc/overview.md
@@ -30,6 +30,7 @@ This is the reference documentation of the Aptos framework.
- [`0x1::governance_proposal`](governance_proposal.md#0x1_governance_proposal)
- [`0x1::guid`](guid.md#0x1_guid)
- [`0x1::managed_coin`](managed_coin.md#0x1_managed_coin)
+- [`0x1::multisig_account`](multisig_account.md#0x1_multisig_account)
- [`0x1::optional_aggregator`](optional_aggregator.md#0x1_optional_aggregator)
- [`0x1::reconfiguration`](reconfiguration.md#0x1_reconfiguration)
- [`0x1::resource_account`](resource_account.md#0x1_resource_account)
diff --git a/aptos-move/framework/aptos-framework/doc/voting.md b/aptos-move/framework/aptos-framework/doc/voting.md
index f92be497e9c45c..423be21b424d5e 100644
--- a/aptos-move/framework/aptos-framework/doc/voting.md
+++ b/aptos-move/framework/aptos-framework/doc/voting.md
@@ -723,8 +723,8 @@ Create a single-step or a multi-step proposal with the given parameters
@param voting_forum_address The forum's address where the proposal will be stored.
@param execution_content The execution content that will be given back at resolution time. This can contain
data such as a capability resource used to scope the execution.
-@param execution_hash The hash for the execution script module. Only the same exact script module can resolve
-this proposal.
+@param execution_hash The sha-256 hash for the execution script module. Only the same exact script module can
+resolve this proposal.
@param min_vote_threshold The minimum number of votes needed to consider this proposal successful.
@param expiration_secs The time in seconds at which the proposal expires and can potentially be resolved.
@param early_resolution_vote_threshold The vote threshold for early resolution of this proposal.
diff --git a/aptos-move/framework/aptos-framework/sources/multisig_account.move b/aptos-move/framework/aptos-framework/sources/multisig_account.move
new file mode 100644
index 00000000000000..fe2972765e1da3
--- /dev/null
+++ b/aptos-move/framework/aptos-framework/sources/multisig_account.move
@@ -0,0 +1,1115 @@
+/// 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 acount nonce).
+///
+/// The flow is like below:
+/// 1. 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.
+/// 2. 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.
+/// 3. To create a new transaction, an owner can call create_transaction with the transaction payload: specified module
+/// (address + name), the name of the function to call, and argument values. This will store the full transaction
+/// payload on chain, which adds decentralization (censorship is not possible) 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 (module + function + args). Later execution will
+/// be verified using the hash. Only owners can create transactions and a transaction id (incremeting id) will be
+/// assigned.
+/// 4. To approve or reject a transaction, other owners can call approve() or reject() with the transaction id.
+/// 5. 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.
+/// 6. If there are enough rejections, any owner can remove the transaction by calling remove().
+module aptos_framework::multisig_account {
+ use aptos_framework::account::{Self, SignerCapability, new_event_handle, create_resource_address};
+ use aptos_framework::aptos_coin::AptosCoin;
+ use aptos_framework::coin;
+ use aptos_framework::event::{EventHandle, emit_event};
+ use aptos_std::simple_map::{Self, SimpleMap};
+ use aptos_std::table::{Self, Table};
+ use std::error;
+ use std::hash::sha3_256;
+ use std::option::{Self, Option};
+ use std::signer::address_of;
+ use std::string::{Self, String};
+ use std::vector;
+
+ /// 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 SALT: vector = b"aptos_framework::multisig_account";
+
+ /// Owner list cannot contain the same address more than once.
+ const EDUPLICATE_OWNER: u64 = 1;
+ /// Specified account is not a multisig account.
+ const EACCOUNT_NOT_MULTISIG: u64 = 2;
+ /// Account executing this operation is not an owner of the multisig account.
+ const ENOT_OWNER: u64 = 3;
+ /// Target function cannot be empty.
+ const ETARGET_FUNCTION_CANNOT_BE_EMPTY: u64 = 4;
+ /// Multisig account must have at least one owner.
+ const ENOT_ENOUGH_OWNERS: u64 = 5;
+ /// Function hash cannot be empty.
+ const EFUNCTION_HASH_CANNOT_BE_EMPTY: u64 = 6;
+ /// Transaction with specified id cannot be found. It either has not been created or has already been executed.
+ const ETRANSACTION_NOT_FOUND: u64 = 7;
+ /// Cannot execute the specified transaction simply via transaction_id as the full payload is not stored on chain.
+ const EPAYLOAD_NOT_STORED: u64 = 8;
+ /// Provided target function does not match the hash stored in the on-chain transaction.
+ const ETARGET_FUNCTION_DOES_NOT_MATCH_HASH: u64 = 9;
+ /// Provided arguments do not match the hash stored in the on-chain transaction.
+ const EARGUMENTS_DOES_NOT_MATCH_HASH: u64 = 10;
+ /// Transaction has not received enough approvals to be executed.
+ const ENOT_ENOUGH_APPROVALS: u64 = 11;
+ /// Transactions have to be approved or rejected in creation order.
+ const ETRANSACTION_CANNOT_BE_EXECUTED_OUT_OF_ORDER: u64 = 12;
+ /// Transaction has not received enough rejections to be removed.
+ const ENOT_ENOUGH_REJECTIONS: u64 = 13;
+ /// Number of signatures required must be more than zero and at most the total number of owners.
+ const EINVALID_SIGNATURES_REQUIRED: u64 = 14;
+
+ /// 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 {
+ // The list of all owner addresses.
+ owners: vector,
+ // The number of signatures required to pass a transaction (k in k-of-n).
+ signatures_required: u64,
+ // Map from transaction id (incrementing id) to transactions to execute for this multisig account.
+ // Already executed transactions are deleted to save on storage but can always be accessed via events.
+ transactions: Table,
+ // Last executed or rejected transaction id. Used to enforce in-order executions of proposals.
+ last_transaction_id: u64,
+ // The transaction id to assign to the next transaction.
+ next_transaction_id: u64,
+ // The signer capability controlling the multisig (resource) account. This can be exchanged for the signer.
+ // Currently not used as the MultisigTransaction can validate and create a signer directly in the VM but
+ // this can be useful to have for on-chain composability in the future.
+ signer_cap: SignerCapability,
+
+ // Events.
+ add_owners_events: EventHandle,
+ remove_owners_events: EventHandle,
+ update_signature_required_events: EventHandle,
+ create_transaction_events: EventHandle,
+ approve_transaction_events: EventHandle,
+ reject_transaction_events: EventHandle,
+ execute_transaction_events: EventHandle,
+ remove_transaction_events: EventHandle,
+ }
+
+ /// 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 {
+ payload: Option,
+ payload_hash: Option,
+ // Owners who have approved. Uses a simple map to deduplicate.
+ approvals: SimpleMap,
+ // Owners who have rejected. Uses a simple map to deduplicate.
+ rejections: SimpleMap,
+ // The owner who created this transaction.
+ creator: address,
+ }
+
+ /// The payload of the transaction to store on chain.
+ struct TransactionPayload has copy, drop, store {
+ // The target function to call such as 0x123::module_to_call::function_to_call.
+ target_function: String,
+ // BCS-encoded argument values to invoke the target function with.
+ args: vector,
+ }
+
+ /// The hash of the multisig transaction payload.
+ struct PayloadHash has copy, drop, store {
+ // Hash of the function to call
+ function_hash: vector,
+ // Hash of the arguments, concatenated in the right order.
+ args_hash: vector,
+ }
+
+ /// Event emitted when new owners are added to the multisig account.
+ struct AddOwnersEvent has drop, store {
+ owners_added: vector,
+ }
+
+ /// Event emitted when new owners are removed from the multisig account.
+ struct RemoveOwnersEvent has drop, store {
+ owners_removed: vector,
+ }
+
+ /// Event emitted when the number of signatures required is updated.
+ struct UpdateSignaturesRequiredEvent has drop, store {
+ old_signatures_required: u64,
+ new_signatures_required: u64,
+ }
+
+ /// Event emitted when a transaction is created.
+ struct CreateTransactionEvent has drop, store {
+ transaction_id: u64,
+ transaction: MultisigTransaction,
+ creator: address,
+ }
+
+ /// Event emitted when an owner approves a transaction.
+ struct ApproveTransactionEvent has drop, store {
+ transaction_id: u64,
+ owner: address,
+ num_approvals: u64,
+ }
+
+ /// Event emitted when an owner rejects a transaction.
+ struct RejectTransactionEvent has drop, store {
+ transaction_id: u64,
+ owner: address,
+ num_rejections: u64,
+ }
+
+ /// Event emitted when a transaction is officially removed because the number of rejections have reached the
+ /// number of signatures required.
+ struct RemoveTransactionEvent has drop, store {
+ transaction_id: u64,
+ num_rejections: u64,
+ executor: address,
+ }
+
+ /// Event emitted when a transaction is executed.
+ struct ExecuteTransactionEvent has drop, store {
+ transaction_id: u64,
+ transaction_payload: TransactionPayload,
+ num_approvals: u64,
+ executor: address,
+ }
+
+ #[view]
+ public fun signatures_required(mutlisig_account: address): u64 acquires MultisigAccount {
+ borrow_global(mutlisig_account).signatures_required
+ }
+
+ #[view]
+ public fun owners(mutlisig_account: address): vector acquires MultisigAccount {
+ borrow_global(mutlisig_account).owners
+ }
+
+ #[view]
+ public fun get_transaction(
+ mutlisig_account: address,
+ transaction_id: u64,
+ ): MultisigTransaction acquires MultisigAccount {
+ *table::borrow(&borrow_global(mutlisig_account).transactions, transaction_id)
+ }
+
+ #[view]
+ public fun can_be_executed(
+ multisig_account: address, transaction_id: u64): bool acquires MultisigAccount {
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut(multisig_account);
+ let transaction = table::borrow(&mut multisig_account_resource.transactions, transaction_id);
+ transaction_id == multisig_account_resource.last_transaction_id + 1 &&
+ simple_map::length(&transaction.approvals) >= multisig_account_resource.signatures_required
+ }
+
+ #[view]
+ public fun can_be_removed(
+ multisig_account: address, transaction_id: u64): bool acquires MultisigAccount {
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut(multisig_account);
+ let transaction = table::borrow(&mut multisig_account_resource.transactions, transaction_id);
+ transaction_id == multisig_account_resource.last_transaction_id + 1 &&
+ simple_map::length(&transaction.rejections) >= multisig_account_resource.signatures_required
+ }
+
+ #[view]
+ public fun get_possible_multisig_account_address(creator: address, seed: vector): address {
+ create_resource_address(&creator, create_multisig_account_seed(seed))
+ }
+
+ /// Creates a new multisig account and add the signer as a single owner.
+ /// The seed is optional and can be used to create multiple multisig accounts from the same owner account. A good
+ /// seed to use is the number of multisig accounts created so far with the same owner account.
+ public entry fun create(owner: &signer, signatures_required: u64, seed: vector) {
+ create_with_owners(owner, vector[], signatures_required, seed);
+ }
+
+ /// 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 signatures_require 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, signatures_required: u64, seed: vector) {
+ let owner_address = address_of(owner);
+ vector::push_back(&mut additional_owners, owner_address);
+ validate_owners(&additional_owners);
+ assert!(
+ signatures_required > 0 && signatures_required <= vector::length(&additional_owners),
+ error::invalid_argument(EINVALID_SIGNATURES_REQUIRED),
+ );
+
+ let (multisig_signer, multisig_signer_cap) = create_multisig_account(owner, seed);
+ move_to(&multisig_signer, MultisigAccount {
+ owners: additional_owners,
+ signatures_required,
+ transactions: table::new(),
+ // First transaction will start at id 1 instead of 0.
+ last_transaction_id: 0,
+ next_transaction_id: 1,
+ signer_cap: multisig_signer_cap,
+ add_owners_events: new_event_handle(&multisig_signer),
+ remove_owners_events: new_event_handle(&multisig_signer),
+ update_signature_required_events: new_event_handle(&multisig_signer),
+ create_transaction_events: new_event_handle(&multisig_signer),
+ approve_transaction_events: new_event_handle(&multisig_signer),
+ reject_transaction_events: new_event_handle(&multisig_signer),
+ execute_transaction_events: new_event_handle(&multisig_signer),
+ remove_transaction_events: new_event_handle(&multisig_signer),
+ });
+ }
+
+ /// Add new owners to the multisig account. This can only be invoked by the multisig account itself, through the
+ /// proposal flow.
+ public entry fun add_owners(multisig_account: &signer, new_owners: vector) acquires MultisigAccount {
+ // Short circuit if new owners list is empty.
+ // This avoids emitting an event if no changes happen, which is confusing to off-chain components.
+ if (vector::length(&new_owners) == 0) {
+ return
+ };
+
+ let multisig_address = address_of(multisig_account);
+ assert_multisig_account_exists(multisig_address);
+ let multisig_account_resource = borrow_global_mut(multisig_address);
+
+ vector::append(&mut multisig_account_resource.owners, new_owners);
+ validate_owners(&multisig_account_resource.owners);
+ emit_event(&mut multisig_account_resource.add_owners_events, AddOwnersEvent {
+ owners_added: new_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.
+ public entry fun remove_owners(
+ multisig_account: &signer, owners_to_remove: vector) acquires MultisigAccount {
+ // Short circuit if the list of owners to remove is empty.
+ // This avoids emitting an event if no changes happen, which is confusing to off-chain components.
+ if (vector::length(&owners_to_remove) == 0) {
+ return
+ };
+
+ let multisig_address = address_of(multisig_account);
+ assert_multisig_account_exists(multisig_address);
+ let multisig_account_resource = borrow_global_mut(multisig_address);
+
+ let i = 0;
+ let len = vector::length(&owners_to_remove);
+ let owners = &mut multisig_account_resource.owners;
+ let owners_removed = vector::empty();
+ while (i < len) {
+ let owner_to_remove = *vector::borrow(&owners_to_remove, i);
+ let (found, index) = vector::index_of(owners, &owner_to_remove);
+ // Only remove an owner if they're present in the owners list.
+ if (found) {
+ vector::push_back(&mut owners_removed, owner_to_remove);
+ vector::swap_remove(owners, index);
+ };
+ i = i + 1;
+ };
+
+ // Make sure there's still at least 1 owner.
+ assert!(vector::length(owners) > 0, error::invalid_state(ENOT_ENOUGH_OWNERS));
+
+ emit_event(&mut multisig_account_resource.remove_owners_events, RemoveOwnersEvent { owners_removed });
+ }
+
+ /// 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.
+ public entry fun update_signatures_required(
+ multisig_account: &signer, new_signatures_required: u64) acquires MultisigAccount {
+ let multisig_address = address_of(multisig_account);
+ assert_multisig_account_exists(multisig_address);
+ let multisig_account_resource = borrow_global_mut(multisig_address);
+ let num_owners = vector::length(&multisig_account_resource.owners);
+ assert!(
+ new_signatures_required > 0 && new_signatures_required <= num_owners,
+ error::invalid_argument(EINVALID_SIGNATURES_REQUIRED),
+ );
+
+ // Short-circuit if the new number of signatures required is the same as before.
+ // This avoids emitting an event.
+ if (multisig_account_resource.signatures_required == new_signatures_required) {
+ return
+ };
+ let old_signatures_required = multisig_account_resource.signatures_required;
+ multisig_account_resource.signatures_required = new_signatures_required;
+ emit_event(
+ &mut multisig_account_resource.update_signature_required_events,
+ UpdateSignaturesRequiredEvent {
+ old_signatures_required,
+ new_signatures_required,
+ }
+ );
+ }
+
+ /// Create a multisig transaction, which will have one approval initially (from the creator).
+ ///
+ /// @param target_function The target function to call such as 0x123::module_to_call::function_to_call.
+ /// @param args Vector of BCS-encoded argument values to invoke the target function with.
+ public entry fun create_transaction(
+ owner: &signer,
+ multisig_account: address,
+ target_function: String,
+ args: vector,
+ ) acquires MultisigAccount {
+ assert!(string::length(&target_function) > 0, error::invalid_argument(ETARGET_FUNCTION_CANNOT_BE_EMPTY));
+
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut(multisig_account);
+ assert_is_owner(owner, multisig_account_resource);
+
+ let creator = address_of(owner);
+ let transaction = MultisigTransaction {
+ payload: option::some(TransactionPayload { target_function, args }),
+ payload_hash: option::none(),
+ approvals: simple_map::create(),
+ rejections: simple_map::create(),
+ creator,
+ };
+ add_transaction(creator, multisig_account_resource, transaction);
+ }
+
+ /// 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.
+ ///
+ /// @param function_hash The sha-256 hash of the function to invoke, e.g. 0x123::module_to_call::function_to_call.
+ /// @param args_hash The sha-256 hash of the function arguments - a concatenated vector of the bcs-encoded
+ /// function arguments.
+ public entry fun create_transaction_with_hash(
+ owner: &signer,
+ multisig_account: address,
+ function_hash: vector,
+ args_hash: vector,
+ ) acquires MultisigAccount {
+ assert!(vector::length(&function_hash) > 0, error::invalid_argument(EFUNCTION_HASH_CANNOT_BE_EMPTY));
+
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut(multisig_account);
+ assert_is_owner(owner, multisig_account_resource);
+
+ let creator = address_of(owner);
+ let transaction = MultisigTransaction {
+ payload: option::none(),
+ payload_hash: option::some(PayloadHash { function_hash, args_hash }),
+ approvals: simple_map::create(),
+ rejections: simple_map::create(),
+ creator,
+ };
+ add_transaction(creator, multisig_account_resource, transaction);
+ }
+
+ /// Approve a multisig transaction.
+ public entry fun approve_transaction(
+ owner: &signer, multisig_account: address, transaction_id: u64) acquires MultisigAccount {
+ vote_transanction(owner, multisig_account, transaction_id, true);
+ }
+
+ /// Reject a multisig transaction.
+ public entry fun reject_transaction(
+ owner: &signer, multisig_account: address, transaction_id: u64) acquires MultisigAccount {
+ vote_transanction(owner, multisig_account, transaction_id, false);
+ }
+
+ /// Generic function that can be used to either approve or reject a multisig transaction
+ public entry fun vote_transanction(
+ owner: &signer, multisig_account: address, transaction_id: u64, approved: bool) acquires MultisigAccount {
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut(multisig_account);
+ assert_is_owner(owner, multisig_account_resource);
+
+ assert!(
+ table::contains(&multisig_account_resource.transactions, transaction_id),
+ error::not_found(ETRANSACTION_NOT_FOUND),
+ );
+ let transaction = table::borrow_mut(&mut multisig_account_resource.transactions, transaction_id);
+ let owner_addr = address_of(owner);
+ if (approved) {
+ simple_map::add(&mut transaction.approvals, address_of(owner), true);
+ emit_event(
+ &mut multisig_account_resource.approve_transaction_events,
+ ApproveTransactionEvent {
+ owner: owner_addr,
+ transaction_id,
+ num_approvals: simple_map::length(&transaction.approvals),
+ }
+ );
+ } else {
+ simple_map::add(&mut transaction.rejections, address_of(owner), true);
+ emit_event(
+ &mut multisig_account_resource.reject_transaction_events,
+ RejectTransactionEvent {
+ owner: owner_addr,
+ transaction_id,
+ num_rejections: simple_map::length(&transaction.rejections),
+ }
+ );
+ };
+ }
+
+ /// Remove a transaction that has sufficient owner rejections.
+ ///
+ /// @param transaction_id Id of the transaction to execute.
+ public entry fun remove_transaction(
+ owner: &signer,
+ multisig_account: address,
+ transaction_id: u64,
+ ) acquires MultisigAccount {
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut(multisig_account);
+ assert_is_owner(owner, multisig_account_resource);
+ assert!(
+ table::contains(&multisig_account_resource.transactions, transaction_id),
+ error::not_found(ETRANSACTION_NOT_FOUND),
+ );
+ let transaction = table::remove(&mut multisig_account_resource.transactions, transaction_id);
+ assert!(
+ transaction_id == multisig_account_resource.last_transaction_id + 1,
+ error::invalid_argument(ETRANSACTION_CANNOT_BE_EXECUTED_OUT_OF_ORDER),
+ );
+ assert!(
+ simple_map::length(&transaction.rejections) >= multisig_account_resource.signatures_required,
+ error::invalid_state(ENOT_ENOUGH_REJECTIONS),
+ );
+
+ multisig_account_resource.last_transaction_id = transaction_id;
+ emit_event(
+ &mut multisig_account_resource.remove_transaction_events,
+ RemoveTransactionEvent {
+ transaction_id,
+ num_rejections: simple_map::length(&transaction.rejections),
+ executor: address_of(owner),
+ }
+ );
+ }
+
+ /// Execute a transaction. This doesn't actually invoke the target function but simply marks the transaction as
+ /// already been executed (by removing it from the transactions table). Actual function invocation is done as part
+ /// executing the MultisigTransaction.
+ ///
+ /// This function is private so no other code can call this beside the VM itself as part of MultisigTransaction.
+ ///
+ /// @param transaction_id Id of the transaction to execute.
+ /// @param target_function Optional and can be empty if the full transaction payload is stored on chain.
+ /// @param args Optional and can be empty if the full transaction payload is stored on chain.
+ /// @return The transaction payload to execute as the multisig account.
+ fun execute_transaction(
+ owner: &signer,
+ multisig_account: address,
+ transaction_id: u64,
+ target_function: String,
+ args: vector,
+ ): TransactionPayload acquires MultisigAccount {
+ assert_multisig_account_exists(multisig_account);
+ let multisig_account_resource = borrow_global_mut(multisig_account);
+ assert_is_owner(owner, multisig_account_resource);
+ assert!(
+ table::contains(&multisig_account_resource.transactions, transaction_id),
+ error::not_found(ETRANSACTION_NOT_FOUND),
+ );
+ let transaction = table::remove(&mut multisig_account_resource.transactions, transaction_id);
+ assert!(
+ transaction_id == multisig_account_resource.last_transaction_id + 1,
+ error::invalid_argument(ETRANSACTION_CANNOT_BE_EXECUTED_OUT_OF_ORDER),
+ );
+ assert!(
+ simple_map::length(&transaction.approvals) >= multisig_account_resource.signatures_required,
+ error::invalid_state(ENOT_ENOUGH_APPROVALS),
+ );
+
+ let transaction_payload =
+ if (option::is_some(&transaction.payload)) {
+ option::extract(&mut transaction.payload)
+ } else {
+ let payload_hash = option::extract(&mut transaction.payload_hash);
+ assert!(
+ sha3_256(*string::bytes(&target_function)) == payload_hash.function_hash,
+ error::invalid_argument(ETARGET_FUNCTION_DOES_NOT_MATCH_HASH),
+ );
+ assert!(
+ sha3_256(args) == payload_hash.args_hash,
+ error::invalid_argument(EARGUMENTS_DOES_NOT_MATCH_HASH),
+ );
+ TransactionPayload { target_function, args }
+ };
+ multisig_account_resource.last_transaction_id = transaction_id;
+ emit_event(
+ &mut multisig_account_resource.execute_transaction_events,
+ ExecuteTransactionEvent {
+ transaction_id,
+ transaction_payload,
+ num_approvals: simple_map::length(&transaction.approvals),
+ executor: address_of(owner),
+ }
+ );
+
+ transaction_payload
+ }
+
+ fun add_transaction(creator: address, multisig_account: &mut MultisigAccount, transaction: MultisigTransaction) {
+ simple_map::add(&mut transaction.approvals, creator, true);
+
+ let transaction_id = multisig_account.next_transaction_id;
+ multisig_account.next_transaction_id = transaction_id + 1;
+ table::add(&mut multisig_account.transactions, transaction_id, transaction);
+ emit_event(
+ &mut multisig_account.create_transaction_events,
+ CreateTransactionEvent { transaction_id, transaction, creator },
+ );
+ }
+
+ fun create_multisig_account(owner: &signer, seed: vector): (signer, SignerCapability) {
+ let (multisig_signer, multisig_signer_cap) =
+ account::create_resource_account(owner, create_multisig_account_seed(seed));
+ // 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(address_of(&multisig_signer))) {
+ coin::register(&multisig_signer);
+ };
+
+ (multisig_signer, multisig_signer_cap)
+ }
+
+ fun create_multisig_account_seed(seed: vector): vector {
+ // Generate a seed that will be used to create the resource account that hosts the staking contract.
+ let multisig_account_seed = vector::empty();
+ vector::append(&mut multisig_account_seed, SALT);
+ // Add an extra salt given by the owner in case an account with the same address has already been created.
+ vector::append(&mut multisig_account_seed, seed);
+
+ multisig_account_seed
+ }
+
+ fun validate_owners(owners: &vector) {
+ let distinct_owners = simple_map::create();
+ let i = 0;
+ let len = vector::length(owners);
+ while (i < len) {
+ let owner = *vector::borrow(owners, i);
+ assert!(
+ !simple_map::contains_key(&distinct_owners, &owner),
+ error::invalid_argument(EDUPLICATE_OWNER),
+ );
+ simple_map::add(&mut distinct_owners, owner, true);
+ i = i + 1;
+ }
+ }
+
+ fun assert_is_owner(owner: &signer, multisig_account: &MultisigAccount) {
+ assert!(
+ vector::contains(&multisig_account.owners, &address_of(owner)),
+ error::permission_denied(ENOT_OWNER),
+ );
+ }
+
+ fun assert_multisig_account_exists(multisig_account: address) {
+ assert!(exists(multisig_account), error::invalid_state(EACCOUNT_NOT_MULTISIG));
+ }
+
+ #[test_only]
+ use std::string::utf8;
+ #[test_only]
+ use aptos_framework::account::create_signer_for_test;
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ public entry fun test_end_to_end(owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ let seed = vector[1, 2, 3];
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, seed);
+ create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, seed);
+
+ // Create three transactions.
+ create_transaction(owner_1, multisig_account, utf8(b"0x1::coin::transfer"), vector[1, 2, 3]);
+ create_transaction(owner_2, multisig_account, utf8(b"0x1::coin::deposit"), vector[1, 2, 3]);
+ create_transaction_with_hash(owner_3, multisig_account, sha3_256(b"1"), sha3_256(vector[]));
+
+ // Owner 3 doesn't need to explicitly approve as they created the transaction.
+ approve_transaction(owner_1, multisig_account, 3);
+ // Third transaction has 2 approvals but cannot be executed out-of-order.
+ assert!(!can_be_executed(multisig_account, 3), 0);
+
+ // Owner 1 doesn't need to explicitly approve as they created the transaction.
+ approve_transaction(owner_2, multisig_account, 1);
+ // First transaction has 2 approvals so it can be executed.
+ assert!(can_be_executed(multisig_account, 1), 1);
+ execute_transaction(owner_2, multisig_account, 1, utf8(b""), vector[]);
+
+ reject_transaction(owner_1, multisig_account, 2);
+ reject_transaction(owner_3, multisig_account, 2);
+ // Second transaction has 1 approval (owner 3) and 2 rejections (owners 1 & 2) and thus can be removed.
+ assert!(can_be_removed(multisig_account, 2), 2);
+ remove_transaction(owner_1, multisig_account, 2);
+
+ // Third transaction can be executed now.
+ execute_transaction(owner_3, multisig_account, 3, utf8(b"1"), vector[]);
+ }
+
+ #[test(owner = @0x123)]
+ public entry fun test_create_with_single_owner(owner: &signer) acquires MultisigAccount {
+ create(owner, 1, vector[]);
+ let owner_addr = address_of(owner);
+ let multisig_account = get_possible_multisig_account_address(owner_addr, vector[]);
+ assert_multisig_account_exists(multisig_account);
+ assert!(owners(multisig_account) == vector[owner_addr], 0);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ public entry fun test_create_with_as_many_sigs_required_as_num_owners(
+ owner_1: &signer, owner_2: &signer, owner_3: &signer) {
+ create_with_owners(owner_1, vector[address_of(owner_2), address_of(owner_3)], 3, vector[]);
+ let owner_1_addr = address_of(owner_1);
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, vector[]);
+ assert_multisig_account_exists(multisig_account);
+ }
+
+ #[test(owner = @0x123)]
+ #[expected_failure(abort_code = 0x1000e, location = Self)]
+ public entry fun test_create_with_zero_signatures_required_should_fail(owner: &signer) {
+ create(owner, 0, vector[]);
+ }
+
+ #[test(owner = @0x123)]
+ #[expected_failure(abort_code = 0x1000e, location = Self)]
+ public entry fun test_create_with_too_many_signatures_required_should_fail(owner: &signer) {
+ create(owner, 2, vector[]);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ #[expected_failure(abort_code = 0x10001, location = Self)]
+ public entry fun test_create_with_duplicate_owners_should_fail(
+ owner_1: &signer, owner_2: &signer, owner_3: &signer) {
+ create_with_owners(owner_1, vector[
+ // Duplicate owner 2 addresses.
+ address_of(owner_2),
+ address_of(owner_3),
+ address_of(owner_2),
+ ], 2, vector[]);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ #[expected_failure(abort_code = 0x10001, location = Self)]
+ public entry fun test_create_with_creator_in_additional_owners_list_should_fail(
+ owner_1: &signer, owner_2: &signer, owner_3: &signer) {
+ create_with_owners(owner_1, vector[
+ // Duplicate owner 1 addresses.
+ address_of(owner_1),
+ address_of(owner_2),
+ address_of(owner_3),
+ ], 2, vector[]);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ public entry fun test_update_signatures_required(
+ owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ create_with_owners(owner_1, vector[address_of(owner_2), address_of(owner_3)], 1, vector[]);
+ let owner_1_addr = address_of(owner_1);
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, vector[]);
+ assert!(signatures_required(multisig_account) == 1, 0);
+ update_signatures_required(&create_signer_for_test(multisig_account), 2);
+ assert!(signatures_required(multisig_account) == 2, 1);
+ // As many signatures required as number of owners (3).
+ update_signatures_required(&create_signer_for_test(multisig_account), 3);
+ assert!(signatures_required(multisig_account) == 3, 2);
+ }
+
+ #[test(owner = @0x123)]
+ #[expected_failure(abort_code = 0x1000e, location = Self)]
+ public entry fun test_update_with_zero_signatures_required_should_fail(owner:& signer) acquires MultisigAccount {
+ create(owner,1, vector[]);
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ update_signatures_required(&create_signer_for_test(multisig_account), 0);
+ }
+
+ #[test(owner = @0x123)]
+ #[expected_failure(abort_code = 0x1000e, location = Self)]
+ public entry fun test_update_with_too_many_signatures_required_should_fail(
+ owner: &signer) acquires MultisigAccount {
+ create(owner,1, vector[]);
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ update_signatures_required(&create_signer_for_test(multisig_account), 2);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ public entry fun test_add_owners(owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ create(owner_1, 1, vector[]);
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, vector[]);
+ let multisig_signer = &create_signer_for_test(multisig_account);
+ assert!(owners(multisig_account) == vector[owner_1_addr], 0);
+ // Adding an empty vector of new owners should be no-op.
+ add_owners(multisig_signer, vector[]);
+ assert!(owners(multisig_account) == vector[owner_1_addr], 1);
+ add_owners(multisig_signer, vector[owner_2_addr, owner_3_addr]);
+ assert!(owners(multisig_account) == vector[owner_1_addr, owner_2_addr, owner_3_addr], 2);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ public entry fun test_remove_owners(owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 1, vector[]);
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, vector[]);
+ let multisig_signer = &create_signer_for_test(multisig_account);
+ assert!(owners(multisig_account) == vector[owner_2_addr, owner_3_addr, owner_1_addr], 0);
+ // Removing an empty vector of owners should be no-op.
+ remove_owners(multisig_signer, vector[]);
+ assert!(owners(multisig_account) == vector[owner_2_addr, owner_3_addr, owner_1_addr], 1);
+ remove_owners(multisig_signer, vector[owner_2_addr]);
+ assert!(owners(multisig_account) == vector[owner_1_addr, owner_3_addr], 2);
+ // Removing owners that don't exist should be no-op.
+ remove_owners(multisig_signer, vector[@0x130]);
+ assert!(owners(multisig_account) == vector[owner_1_addr, owner_3_addr], 3);
+ // Removing with duplicate owners should still work.
+ remove_owners(multisig_signer, vector[owner_3_addr, owner_3_addr, owner_3_addr]);
+ assert!(owners(multisig_account) == vector[owner_1_addr], 4);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ #[expected_failure(abort_code = 0x30005, location = Self)]
+ public entry fun test_remove_all_owners_should_fail(
+ owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 1, vector[]);
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, vector[]);
+ assert!(owners(multisig_account) == vector[owner_2_addr, owner_3_addr, owner_1_addr], 0);
+ let multisig_signer = &create_signer_for_test(multisig_account);
+ remove_owners(multisig_signer, vector[owner_1_addr, owner_2_addr, owner_3_addr]);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ public entry fun test_create_transaction(
+ owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ let seed = vector[1, 2, 3];
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, seed);
+ create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, seed);
+
+ create_transaction(owner_1, multisig_account, utf8(b"0x1::coin::transfer"), vector[1, 2, 3]);
+ let transaction = get_transaction(multisig_account, 1);
+ assert!(transaction.creator == owner_1_addr, 0);
+ assert!(option::is_some(&transaction.payload), 1);
+ assert!(option::is_none(&transaction.payload_hash), 2);
+ let payload = option::extract(&mut transaction.payload);
+ assert!(payload.target_function == utf8(b"0x1::coin::transfer"), 3);
+ assert!(payload.args == vector[1, 2, 3], 4);
+ assert!(simple_map::length(&transaction.approvals) == 1, 5);
+ assert!(simple_map::length(&transaction.rejections) == 0, 5);
+ }
+
+ #[test(owner = @0x123)]
+ #[expected_failure(abort_code = 0x10004, location = Self)]
+ public entry fun test_create_transaction_with_empty_target_function_should_fail(
+ owner: &signer) acquires MultisigAccount {
+ create(owner,1, vector[]);
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ create_transaction(owner, multisig_account, utf8(b""), vector[1, 2, 3]);
+ }
+
+ #[test(owner = @0x123, non_owner = @0x124)]
+ #[expected_failure(abort_code = 0x50003, location = Self)]
+ public entry fun test_create_transaction_with_non_owner_should_fail(
+ owner: &signer, non_owner: &signer) acquires MultisigAccount {
+ create(owner,1, vector[]);
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ create_transaction(non_owner, multisig_account, utf8(b"test"), vector[]);
+ }
+
+ #[test(owner = @0x123)]
+ public entry fun test_create_transaction_with_hashes(owner: &signer) acquires MultisigAccount {
+ create(owner,1, vector[]);
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ create_transaction_with_hash(owner, multisig_account, sha3_256(b"1"), sha3_256(vector[]));
+ }
+
+ #[test(owner = @0x123)]
+ #[expected_failure(abort_code = 0x10006, location = Self)]
+ public entry fun test_create_transaction_with_empty_function_hash_should_fail(
+ owner: &signer) acquires MultisigAccount {
+ create(owner,1, vector[]);
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ create_transaction_with_hash(owner, multisig_account, b"", sha3_256(vector[]));
+ }
+
+ #[test(owner = @0x123, non_owner = @0x124)]
+ #[expected_failure(abort_code = 0x50003, location = Self)]
+ public entry fun test_create_transaction_with_hashes_and_non_owner_should_fail(
+ owner: &signer, non_owner: &signer) acquires MultisigAccount {
+ create(owner,1, vector[]);
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ create_transaction_with_hash(non_owner, multisig_account, sha3_256(b"1"), sha3_256(vector[]));
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ public entry fun test_approve_transaction(
+ owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ let seed = vector[1, 2, 3];
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, seed);
+ create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, seed);
+
+ create_transaction(owner_1, multisig_account, utf8(b"0x1::coin::transfer"), vector[1, 2, 3]);
+ approve_transaction(owner_2, multisig_account, 1);
+ approve_transaction(owner_3, multisig_account, 1);
+ let transaction = get_transaction(multisig_account, 1);
+ assert!(simple_map::length(&transaction.approvals) == 3, 0);
+ assert!(*simple_map::borrow(&transaction.approvals, &owner_1_addr), 1);
+ assert!(*simple_map::borrow(&transaction.approvals, &owner_2_addr), 2);
+ assert!(*simple_map::borrow(&transaction.approvals, &owner_3_addr), 3);
+ }
+
+ #[test(owner = @0x123)]
+ #[expected_failure(abort_code = 0x60007, location = Self)]
+ public entry fun test_approve_transaction_with_invalid_transaction_id_should_fail(
+ owner: &signer) acquires MultisigAccount {
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ create(owner, 1, vector[]);
+ approve_transaction(owner, multisig_account, 0);
+ }
+
+ #[test(owner = @0x123, non_owner = @0x124)]
+ #[expected_failure(abort_code = 0x50003, location = Self)]
+ public entry fun test_approve_transaction_with_non_owner_should_fail(
+ owner: &signer, non_owner: &signer) acquires MultisigAccount {
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ create(owner, 1, vector[]);
+ approve_transaction(non_owner, multisig_account, 1);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ public entry fun test_reject_transaction(
+ owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ let seed = vector[1, 2, 3];
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, seed);
+ create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, seed);
+
+ create_transaction(owner_1, multisig_account, utf8(b"0x1::coin::transfer"), vector[1, 2, 3]);
+ reject_transaction(owner_1, multisig_account, 1);
+ reject_transaction(owner_2, multisig_account, 1);
+ reject_transaction(owner_3, multisig_account, 1);
+ let transaction = get_transaction(multisig_account, 1);
+ assert!(simple_map::length(&transaction.rejections) == 3, 0);
+ assert!(*simple_map::borrow(&transaction.rejections, &owner_1_addr), 1);
+ assert!(*simple_map::borrow(&transaction.rejections, &owner_2_addr), 2);
+ assert!(*simple_map::borrow(&transaction.rejections, &owner_3_addr), 3);
+ }
+
+ #[test(owner = @0x123)]
+ #[expected_failure(abort_code = 0x60007, location = Self)]
+ public entry fun test_reject_transaction_with_invalid_transaction_id_should_fail(
+ owner: &signer) acquires MultisigAccount {
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ create(owner, 1, vector[]);
+ reject_transaction(owner, multisig_account, 0);
+ }
+
+ #[test(owner = @0x123, non_owner = @0x124)]
+ #[expected_failure(abort_code = 0x50003, location = Self)]
+ public entry fun test_reject_transaction_with_non_owner_should_fail(
+ owner: &signer, non_owner: &signer) acquires MultisigAccount {
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ create(owner, 1, vector[]);
+ reject_transaction(non_owner, multisig_account, 1);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ public entry fun test_execute_transaction(
+ owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ let seed = vector[1, 2, 3];
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, seed);
+ create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, seed);
+
+ create_transaction(owner_1, multisig_account, utf8(b"0x1::coin::transfer"), vector[1, 2, 3]);
+ // Owner 1 doesn't need to explicitly approve as they created the transaction.
+ approve_transaction(owner_2, multisig_account, 1);
+ assert!(can_be_executed(multisig_account, 1), 1);
+ assert!(table::contains(&borrow_global(multisig_account).transactions, 1), 0);
+ execute_transaction(owner_3, multisig_account, 1, utf8(b""), vector[]);
+ assert!(!table::contains(&borrow_global(multisig_account).transactions, 1), 1);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ public entry fun test_execute_transaction_with_full_payload(
+ owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ let seed = vector[1, 2, 3];
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, seed);
+ create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, seed);
+
+ create_transaction_with_hash(owner_3, multisig_account, sha3_256(b"1"), sha3_256(vector[]));
+ // Owner 3 doesn't need to explicitly approve as they created the transaction.
+ approve_transaction(owner_1, multisig_account, 1);
+ assert!(can_be_executed(multisig_account, 1), 1);
+ assert!(table::contains(&borrow_global(multisig_account).transactions, 1), 0);
+ execute_transaction(owner_3, multisig_account, 1, utf8(b"1"), vector[]);
+ assert!(!table::contains(&borrow_global(multisig_account).transactions, 1), 1);
+ }
+
+ #[test(owner = @0x123, non_owner = @0x124)]
+ #[expected_failure(abort_code = 0x50003, location = Self)]
+ public entry fun test_execute_transaction_with_non_owner_should_fail(
+ owner: &signer, non_owner: &signer) acquires MultisigAccount {
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ create(owner,1, vector[]);
+
+ create_transaction(owner, multisig_account, utf8(b"0x1::coin::transfer"), vector[1, 2, 3]);
+ assert!(can_be_executed(multisig_account, 1), 1);
+ execute_transaction(non_owner, multisig_account, 1, utf8(b""), vector[]);
+ }
+
+ #[test(owner = @0x123)]
+ #[expected_failure(abort_code = 0x60007, location = Self)]
+ public entry fun test_execute_transaction_with_invalid_transaction_id_should_fail(
+ owner: &signer) acquires MultisigAccount {
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ create(owner,1, vector[]);
+
+ create_transaction(owner, multisig_account, utf8(b"0x1::coin::transfer"), vector[1, 2, 3]);
+ execute_transaction(owner, multisig_account, 2, utf8(b""), vector[]);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ #[expected_failure(abort_code = 0x3000B, location = Self)]
+ public entry fun test_execute_transaction_without_sufficient_approvals_should_fail(
+ owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ let seed = vector[1, 2, 3];
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, seed);
+ create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, seed);
+
+ create_transaction_with_hash(owner_3, multisig_account, sha3_256(b"1"), sha3_256(vector[]));
+ execute_transaction(owner_3, multisig_account, 1, utf8(b"1"), vector[]);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ #[expected_failure(abort_code = 0x1000C, location = Self)]
+ public entry fun test_execute_transaction_out_of_order_should_fail(
+ owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ let seed = vector[1, 2, 3];
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, seed);
+ create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, seed);
+
+ create_transaction(owner_1, multisig_account, utf8(b"1"),vector[]);
+ create_transaction_with_hash(owner_3, multisig_account, sha3_256(b"1"), sha3_256(vector[]));
+ approve_transaction(owner_2, multisig_account, 1);
+ execute_transaction(owner_3, multisig_account, 2, utf8(b"1"), vector[]);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ public entry fun test_remove_transaction(
+ owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ let seed = vector[1, 2, 3];
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, seed);
+ create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, seed);
+
+ create_transaction(owner_1, multisig_account, utf8(b"0x1::coin::transfer"), vector[1, 2, 3]);
+ reject_transaction(owner_2, multisig_account, 1);
+ reject_transaction(owner_3, multisig_account, 1);
+ assert!(can_be_removed(multisig_account, 1), 1);
+ assert!(table::contains(&borrow_global(multisig_account).transactions, 1), 0);
+ remove_transaction(owner_3, multisig_account, 1);
+ assert!(!table::contains(&borrow_global(multisig_account).transactions, 1), 1);
+ }
+
+ #[test(owner = @0x123)]
+ #[expected_failure(abort_code = 0x60007, location = Self)]
+ public entry fun test_remove_transaction_with_invalid_transaction_id_should_fail(
+ owner: &signer) acquires MultisigAccount {
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ create(owner,1, vector[]);
+
+ create_transaction(owner, multisig_account, utf8(b"0x1::coin::transfer"), vector[1, 2, 3]);
+ reject_transaction(owner, multisig_account, 1);
+ remove_transaction(owner, multisig_account, 2);
+ }
+
+ #[test(owner = @0x123, non_owner = @0x124)]
+ #[expected_failure(abort_code = 0x50003, location = Self)]
+ public entry fun test_remove_transaction_with_non_owner_should_fail(
+ owner: &signer, non_owner: &signer) acquires MultisigAccount {
+ let multisig_account = get_possible_multisig_account_address(address_of(owner), vector[]);
+ create(owner,1, vector[]);
+
+ create_transaction(owner, multisig_account, utf8(b"0x1::coin::transfer"), vector[1, 2, 3]);
+ reject_transaction(owner, multisig_account, 1);
+ remove_transaction(non_owner, multisig_account, 1);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ #[expected_failure(abort_code = 0x3000D, location = Self)]
+ public entry fun test_remove_transaction_without_sufficient_rejections_should_fail(owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ let seed = vector[1, 2, 3];
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, seed);
+ create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, seed);
+
+ create_transaction(owner_1, multisig_account, utf8(b"0x1::coin::transfer"), vector[1, 2, 3]);
+ reject_transaction(owner_2, multisig_account, 1);
+ remove_transaction(owner_3, multisig_account, 1);
+ }
+
+ #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)]
+ #[expected_failure(abort_code = 0x1000C, location = Self)]
+ public entry fun test_remove_transaction_out_of_order_should_fail(owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount {
+ let owner_1_addr = address_of(owner_1);
+ let owner_2_addr = address_of(owner_2);
+ let owner_3_addr = address_of(owner_3);
+ let seed = vector[1, 2, 3];
+ let multisig_account = get_possible_multisig_account_address(owner_1_addr, seed);
+ create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, seed);
+
+ create_transaction(owner_1, multisig_account, utf8(b"1"),vector[]);
+ create_transaction_with_hash(owner_3, multisig_account, sha3_256(b"1"), sha3_256(vector[]));
+ reject_transaction(owner_1, multisig_account, 1);
+ reject_transaction(owner_2, multisig_account, 1);
+ remove_transaction(owner_3, multisig_account, 2);
+ }
+}
diff --git a/aptos-move/framework/aptos-framework/sources/voting.move b/aptos-move/framework/aptos-framework/sources/voting.move
index fdc8bb28e7172c..318ab4b70a7938 100644
--- a/aptos-move/framework/aptos-framework/sources/voting.move
+++ b/aptos-move/framework/aptos-framework/sources/voting.move
@@ -232,8 +232,8 @@ module aptos_framework::voting {
/// @param voting_forum_address The forum's address where the proposal will be stored.
/// @param execution_content The execution content that will be given back at resolution time. This can contain
/// data such as a capability resource used to scope the execution.
- /// @param execution_hash The hash for the execution script module. Only the same exact script module can resolve
- /// this proposal.
+ /// @param execution_hash The sha-256 hash for the execution script module. Only the same exact script module can
+ /// resolve this proposal.
/// @param min_vote_threshold The minimum number of votes needed to consider this proposal successful.
/// @param expiration_secs The time in seconds at which the proposal expires and can potentially be resolved.
/// @param early_resolution_vote_threshold The vote threshold for early resolution of this proposal.
diff --git a/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs b/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs
index c1445729588bd3..4a931b420e745b 100644
--- a/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs
+++ b/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs
@@ -212,6 +212,96 @@ pub enum EntryFunctionCall {
coin_type: TypeTag,
},
+ /// Add new owners to the multisig account. This can only be invoked by the multisig account itself, through the
+ /// proposal flow.
+ MultisigAccountAddOwners {
+ new_owners: Vec,
+ },
+
+ /// Approve a multisig transaction.
+ MultisigAccountApproveTransaction {
+ multisig_account: AccountAddress,
+ transaction_id: u64,
+ },
+
+ /// Creates a new multisig account and add the signer as a single owner.
+ /// The seed is optional and can be used to create multiple multisig accounts from the same owner account. A good
+ /// seed to use is the number of multisig accounts created so far with the same owner account.
+ MultisigAccountCreate {
+ signatures_required: u64,
+ seed: Vec,
+ },
+
+ /// Create a multisig transaction, which will have one approval initially (from the creator).
+ ///
+ /// @param target_function The target function to call such as 0x123::module_to_call::function_to_call.
+ /// @param args Vector of BCS-encoded argument values to invoke the target function with.
+ MultisigAccountCreateTransaction {
+ multisig_account: AccountAddress,
+ target_function: Vec,
+ args: Vec,
+ },
+
+ /// 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.
+ ///
+ /// @param function_hash The sha-256 hash of the function to invoke, e.g. 0x123::module_to_call::function_to_call.
+ /// @param args_hash The sha-256 hash of the function arguments - a concatenated vector of the bcs-encoded
+ /// function arguments.
+ MultisigAccountCreateTransactionWithHash {
+ multisig_account: AccountAddress,
+ function_hash: Vec,
+ args_hash: Vec,
+ },
+
+ /// 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 signatures_require The number of signatures required to execute a transaction. Must be at least 1 and
+ /// at most the total number of owners.
+ MultisigAccountCreateWithOwners {
+ additional_owners: Vec,
+ signatures_required: u64,
+ seed: Vec,
+ },
+
+ /// Reject a multisig transaction.
+ MultisigAccountRejectTransaction {
+ multisig_account: AccountAddress,
+ transaction_id: u64,
+ },
+
+ /// 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.
+ MultisigAccountRemoveOwners {
+ owners_to_remove: Vec,
+ },
+
+ /// Remove a transaction that has sufficient owner rejections.
+ ///
+ /// @param transaction_id Id of the transaction to execute.
+ MultisigAccountRemoveTransaction {
+ multisig_account: AccountAddress,
+ transaction_id: u64,
+ },
+
+ /// 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.
+ MultisigAccountUpdateSignaturesRequired {
+ new_signatures_required: u64,
+ },
+
+ /// Generic function that can be used to either approve or reject a multisig transaction
+ MultisigAccountVoteTransanction {
+ multisig_account: AccountAddress,
+ transaction_id: u64,
+ approved: bool,
+ },
+
/// Creates a new resource account and rotates the authentication key to either
/// the optional auth key if it is non-empty (though auth keys are 32-bytes)
/// or the source accounts current auth key.
@@ -613,6 +703,53 @@ impl EntryFunctionCall {
amount,
} => managed_coin_mint(coin_type, dst_addr, amount),
ManagedCoinRegister { coin_type } => managed_coin_register(coin_type),
+ MultisigAccountAddOwners { new_owners } => multisig_account_add_owners(new_owners),
+ MultisigAccountApproveTransaction {
+ multisig_account,
+ transaction_id,
+ } => multisig_account_approve_transaction(multisig_account, transaction_id),
+ MultisigAccountCreate {
+ signatures_required,
+ seed,
+ } => multisig_account_create(signatures_required, seed),
+ MultisigAccountCreateTransaction {
+ multisig_account,
+ target_function,
+ args,
+ } => multisig_account_create_transaction(multisig_account, target_function, args),
+ MultisigAccountCreateTransactionWithHash {
+ multisig_account,
+ function_hash,
+ args_hash,
+ } => multisig_account_create_transaction_with_hash(
+ multisig_account,
+ function_hash,
+ args_hash,
+ ),
+ MultisigAccountCreateWithOwners {
+ additional_owners,
+ signatures_required,
+ seed,
+ } => multisig_account_create_with_owners(additional_owners, signatures_required, seed),
+ MultisigAccountRejectTransaction {
+ multisig_account,
+ transaction_id,
+ } => multisig_account_reject_transaction(multisig_account, transaction_id),
+ MultisigAccountRemoveOwners { owners_to_remove } => {
+ multisig_account_remove_owners(owners_to_remove)
+ }
+ MultisigAccountRemoveTransaction {
+ multisig_account,
+ transaction_id,
+ } => multisig_account_remove_transaction(multisig_account, transaction_id),
+ MultisigAccountUpdateSignaturesRequired {
+ new_signatures_required,
+ } => multisig_account_update_signatures_required(new_signatures_required),
+ MultisigAccountVoteTransanction {
+ multisig_account,
+ transaction_id,
+ approved,
+ } => multisig_account_vote_transanction(multisig_account, transaction_id, approved),
ResourceAccountCreateResourceAccount {
seed,
optional_auth_key,
@@ -1309,6 +1446,260 @@ pub fn managed_coin_register(coin_type: TypeTag) -> TransactionPayload {
))
}
+/// Add new owners to the multisig account. This can only be invoked by the multisig account itself, through the
+/// proposal flow.
+pub fn multisig_account_add_owners(new_owners: Vec) -> TransactionPayload {
+ TransactionPayload::EntryFunction(EntryFunction::new(
+ ModuleId::new(
+ AccountAddress::new([
+ 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, 1,
+ ]),
+ ident_str!("multisig_account").to_owned(),
+ ),
+ ident_str!("add_owners").to_owned(),
+ vec![],
+ vec![bcs::to_bytes(&new_owners).unwrap()],
+ ))
+}
+
+/// Approve a multisig transaction.
+pub fn multisig_account_approve_transaction(
+ multisig_account: AccountAddress,
+ transaction_id: u64,
+) -> TransactionPayload {
+ TransactionPayload::EntryFunction(EntryFunction::new(
+ ModuleId::new(
+ AccountAddress::new([
+ 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, 1,
+ ]),
+ ident_str!("multisig_account").to_owned(),
+ ),
+ ident_str!("approve_transaction").to_owned(),
+ vec![],
+ vec![
+ bcs::to_bytes(&multisig_account).unwrap(),
+ bcs::to_bytes(&transaction_id).unwrap(),
+ ],
+ ))
+}
+
+/// Creates a new multisig account and add the signer as a single owner.
+/// The seed is optional and can be used to create multiple multisig accounts from the same owner account. A good
+/// seed to use is the number of multisig accounts created so far with the same owner account.
+pub fn multisig_account_create(signatures_required: u64, seed: Vec) -> TransactionPayload {
+ TransactionPayload::EntryFunction(EntryFunction::new(
+ ModuleId::new(
+ AccountAddress::new([
+ 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, 1,
+ ]),
+ ident_str!("multisig_account").to_owned(),
+ ),
+ ident_str!("create").to_owned(),
+ vec![],
+ vec![
+ bcs::to_bytes(&signatures_required).unwrap(),
+ bcs::to_bytes(&seed).unwrap(),
+ ],
+ ))
+}
+
+/// Create a multisig transaction, which will have one approval initially (from the creator).
+///
+/// @param target_function The target function to call such as 0x123::module_to_call::function_to_call.
+/// @param args Vector of BCS-encoded argument values to invoke the target function with.
+pub fn multisig_account_create_transaction(
+ multisig_account: AccountAddress,
+ target_function: Vec,
+ args: Vec,
+) -> TransactionPayload {
+ TransactionPayload::EntryFunction(EntryFunction::new(
+ ModuleId::new(
+ AccountAddress::new([
+ 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, 1,
+ ]),
+ ident_str!("multisig_account").to_owned(),
+ ),
+ ident_str!("create_transaction").to_owned(),
+ vec![],
+ vec![
+ bcs::to_bytes(&multisig_account).unwrap(),
+ bcs::to_bytes(&target_function).unwrap(),
+ bcs::to_bytes(&args).unwrap(),
+ ],
+ ))
+}
+
+/// 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.
+///
+/// @param function_hash The sha-256 hash of the function to invoke, e.g. 0x123::module_to_call::function_to_call.
+/// @param args_hash The sha-256 hash of the function arguments - a concatenated vector of the bcs-encoded
+/// function arguments.
+pub fn multisig_account_create_transaction_with_hash(
+ multisig_account: AccountAddress,
+ function_hash: Vec,
+ args_hash: Vec,
+) -> TransactionPayload {
+ TransactionPayload::EntryFunction(EntryFunction::new(
+ ModuleId::new(
+ AccountAddress::new([
+ 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, 1,
+ ]),
+ ident_str!("multisig_account").to_owned(),
+ ),
+ ident_str!("create_transaction_with_hash").to_owned(),
+ vec![],
+ vec![
+ bcs::to_bytes(&multisig_account).unwrap(),
+ bcs::to_bytes(&function_hash).unwrap(),
+ bcs::to_bytes(&args_hash).unwrap(),
+ ],
+ ))
+}
+
+/// 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 signatures_require The number of signatures required to execute a transaction. Must be at least 1 and
+/// at most the total number of owners.
+pub fn multisig_account_create_with_owners(
+ additional_owners: Vec,
+ signatures_required: u64,
+ seed: Vec,
+) -> TransactionPayload {
+ TransactionPayload::EntryFunction(EntryFunction::new(
+ ModuleId::new(
+ AccountAddress::new([
+ 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, 1,
+ ]),
+ ident_str!("multisig_account").to_owned(),
+ ),
+ ident_str!("create_with_owners").to_owned(),
+ vec![],
+ vec![
+ bcs::to_bytes(&additional_owners).unwrap(),
+ bcs::to_bytes(&signatures_required).unwrap(),
+ bcs::to_bytes(&seed).unwrap(),
+ ],
+ ))
+}
+
+/// Reject a multisig transaction.
+pub fn multisig_account_reject_transaction(
+ multisig_account: AccountAddress,
+ transaction_id: u64,
+) -> TransactionPayload {
+ TransactionPayload::EntryFunction(EntryFunction::new(
+ ModuleId::new(
+ AccountAddress::new([
+ 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, 1,
+ ]),
+ ident_str!("multisig_account").to_owned(),
+ ),
+ ident_str!("reject_transaction").to_owned(),
+ vec![],
+ vec![
+ bcs::to_bytes(&multisig_account).unwrap(),
+ bcs::to_bytes(&transaction_id).unwrap(),
+ ],
+ ))
+}
+
+/// 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.
+pub fn multisig_account_remove_owners(owners_to_remove: Vec) -> TransactionPayload {
+ TransactionPayload::EntryFunction(EntryFunction::new(
+ ModuleId::new(
+ AccountAddress::new([
+ 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, 1,
+ ]),
+ ident_str!("multisig_account").to_owned(),
+ ),
+ ident_str!("remove_owners").to_owned(),
+ vec![],
+ vec![bcs::to_bytes(&owners_to_remove).unwrap()],
+ ))
+}
+
+/// Remove a transaction that has sufficient owner rejections.
+///
+/// @param transaction_id Id of the transaction to execute.
+pub fn multisig_account_remove_transaction(
+ multisig_account: AccountAddress,
+ transaction_id: u64,
+) -> TransactionPayload {
+ TransactionPayload::EntryFunction(EntryFunction::new(
+ ModuleId::new(
+ AccountAddress::new([
+ 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, 1,
+ ]),
+ ident_str!("multisig_account").to_owned(),
+ ),
+ ident_str!("remove_transaction").to_owned(),
+ vec![],
+ vec![
+ bcs::to_bytes(&multisig_account).unwrap(),
+ bcs::to_bytes(&transaction_id).unwrap(),
+ ],
+ ))
+}
+
+/// 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.
+pub fn multisig_account_update_signatures_required(
+ new_signatures_required: u64,
+) -> TransactionPayload {
+ TransactionPayload::EntryFunction(EntryFunction::new(
+ ModuleId::new(
+ AccountAddress::new([
+ 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, 1,
+ ]),
+ ident_str!("multisig_account").to_owned(),
+ ),
+ ident_str!("update_signatures_required").to_owned(),
+ vec![],
+ vec![bcs::to_bytes(&new_signatures_required).unwrap()],
+ ))
+}
+
+/// Generic function that can be used to either approve or reject a multisig transaction
+pub fn multisig_account_vote_transanction(
+ multisig_account: AccountAddress,
+ transaction_id: u64,
+ approved: bool,
+) -> TransactionPayload {
+ TransactionPayload::EntryFunction(EntryFunction::new(
+ ModuleId::new(
+ AccountAddress::new([
+ 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, 1,
+ ]),
+ ident_str!("multisig_account").to_owned(),
+ ),
+ ident_str!("vote_transanction").to_owned(),
+ vec![],
+ vec![
+ bcs::to_bytes(&multisig_account).unwrap(),
+ bcs::to_bytes(&transaction_id).unwrap(),
+ bcs::to_bytes(&approved).unwrap(),
+ ],
+ ))
+}
+
/// Creates a new resource account and rotates the authentication key to either
/// the optional auth key if it is non-empty (though auth keys are 32-bytes)
/// or the source accounts current auth key.
@@ -2560,6 +2951,148 @@ mod decoder {
}
}
+ pub fn multisig_account_add_owners(payload: &TransactionPayload) -> Option {
+ if let TransactionPayload::EntryFunction(script) = payload {
+ Some(EntryFunctionCall::MultisigAccountAddOwners {
+ new_owners: bcs::from_bytes(script.args().get(0)?).ok()?,
+ })
+ } else {
+ None
+ }
+ }
+
+ pub fn multisig_account_approve_transaction(
+ payload: &TransactionPayload,
+ ) -> Option {
+ if let TransactionPayload::EntryFunction(script) = payload {
+ Some(EntryFunctionCall::MultisigAccountApproveTransaction {
+ multisig_account: bcs::from_bytes(script.args().get(0)?).ok()?,
+ transaction_id: bcs::from_bytes(script.args().get(1)?).ok()?,
+ })
+ } else {
+ None
+ }
+ }
+
+ pub fn multisig_account_create(payload: &TransactionPayload) -> Option {
+ if let TransactionPayload::EntryFunction(script) = payload {
+ Some(EntryFunctionCall::MultisigAccountCreate {
+ signatures_required: bcs::from_bytes(script.args().get(0)?).ok()?,
+ seed: bcs::from_bytes(script.args().get(1)?).ok()?,
+ })
+ } else {
+ None
+ }
+ }
+
+ pub fn multisig_account_create_transaction(
+ payload: &TransactionPayload,
+ ) -> Option {
+ if let TransactionPayload::EntryFunction(script) = payload {
+ Some(EntryFunctionCall::MultisigAccountCreateTransaction {
+ multisig_account: bcs::from_bytes(script.args().get(0)?).ok()?,
+ target_function: bcs::from_bytes(script.args().get(1)?).ok()?,
+ args: bcs::from_bytes(script.args().get(2)?).ok()?,
+ })
+ } else {
+ None
+ }
+ }
+
+ pub fn multisig_account_create_transaction_with_hash(
+ payload: &TransactionPayload,
+ ) -> Option {
+ if let TransactionPayload::EntryFunction(script) = payload {
+ Some(
+ EntryFunctionCall::MultisigAccountCreateTransactionWithHash {
+ multisig_account: bcs::from_bytes(script.args().get(0)?).ok()?,
+ function_hash: bcs::from_bytes(script.args().get(1)?).ok()?,
+ args_hash: bcs::from_bytes(script.args().get(2)?).ok()?,
+ },
+ )
+ } else {
+ None
+ }
+ }
+
+ pub fn multisig_account_create_with_owners(
+ payload: &TransactionPayload,
+ ) -> Option {
+ if let TransactionPayload::EntryFunction(script) = payload {
+ Some(EntryFunctionCall::MultisigAccountCreateWithOwners {
+ additional_owners: bcs::from_bytes(script.args().get(0)?).ok()?,
+ signatures_required: bcs::from_bytes(script.args().get(1)?).ok()?,
+ seed: bcs::from_bytes(script.args().get(2)?).ok()?,
+ })
+ } else {
+ None
+ }
+ }
+
+ pub fn multisig_account_reject_transaction(
+ payload: &TransactionPayload,
+ ) -> Option {
+ if let TransactionPayload::EntryFunction(script) = payload {
+ Some(EntryFunctionCall::MultisigAccountRejectTransaction {
+ multisig_account: bcs::from_bytes(script.args().get(0)?).ok()?,
+ transaction_id: bcs::from_bytes(script.args().get(1)?).ok()?,
+ })
+ } else {
+ None
+ }
+ }
+
+ pub fn multisig_account_remove_owners(
+ payload: &TransactionPayload,
+ ) -> Option {
+ if let TransactionPayload::EntryFunction(script) = payload {
+ Some(EntryFunctionCall::MultisigAccountRemoveOwners {
+ owners_to_remove: bcs::from_bytes(script.args().get(0)?).ok()?,
+ })
+ } else {
+ None
+ }
+ }
+
+ pub fn multisig_account_remove_transaction(
+ payload: &TransactionPayload,
+ ) -> Option {
+ if let TransactionPayload::EntryFunction(script) = payload {
+ Some(EntryFunctionCall::MultisigAccountRemoveTransaction {
+ multisig_account: bcs::from_bytes(script.args().get(0)?).ok()?,
+ transaction_id: bcs::from_bytes(script.args().get(1)?).ok()?,
+ })
+ } else {
+ None
+ }
+ }
+
+ pub fn multisig_account_update_signatures_required(
+ payload: &TransactionPayload,
+ ) -> Option {
+ if let TransactionPayload::EntryFunction(script) = payload {
+ Some(EntryFunctionCall::MultisigAccountUpdateSignaturesRequired {
+ new_signatures_required: bcs::from_bytes(script.args().get(0)?).ok()?,
+ })
+ } else {
+ None
+ }
+ }
+
+ pub fn multisig_account_vote_transanction(
+ payload: &TransactionPayload,
+ ) -> Option {
+ if let TransactionPayload::EntryFunction(script) = payload {
+ Some(EntryFunctionCall::MultisigAccountVoteTransanction {
+ multisig_account: bcs::from_bytes(script.args().get(0)?).ok()?,
+ transaction_id: bcs::from_bytes(script.args().get(1)?).ok()?,
+ approved: bcs::from_bytes(script.args().get(2)?).ok()?,
+ })
+ } else {
+ None
+ }
+ }
+
pub fn resource_account_create_resource_account(
payload: &TransactionPayload,
) -> Option {
@@ -3233,6 +3766,50 @@ static SCRIPT_FUNCTION_DECODER_MAP: once_cell::sync::Lazy