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