From 7ba26a21875c9bf896295ff9297724c8080ac979 Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 16 Dec 2022 14:32:16 +0700 Subject: [PATCH 1/9] [Aptos Framework] Add multisig account module --- .../framework/aptos-framework/doc/account.md | 87 +- .../aptos-framework/doc/multisig_account.md | 2128 +++++++++++++++++ .../framework/aptos-framework/doc/overview.md | 1 + .../framework/aptos-framework/doc/voting.md | 4 +- .../aptos-framework/sources/account.move | 81 +- .../sources/create_signer.move | 1 + .../sources/multisig_account.move | 1438 +++++++++++ .../aptos-framework/sources/voting.move | 4 +- .../src/aptos_framework_sdk_builder.rs | 642 +++++ 9 files changed, 4333 insertions(+), 53 deletions(-) create mode 100644 aptos-move/framework/aptos-framework/doc/multisig_account.md create mode 100644 aptos-move/framework/aptos-framework/sources/multisig_account.move diff --git a/aptos-move/framework/aptos-framework/doc/account.md b/aptos-move/framework/aptos-framework/doc/account.md index 5a74228946a5b..09b387e59f3c8 100644 --- a/aptos-move/framework/aptos-framework/doc/account.md +++ b/aptos-move/framework/aptos-framework/doc/account.md @@ -48,6 +48,7 @@ - [Function `register_coin`](#0x1_account_register_coin) - [Function `create_signer_with_capability`](#0x1_account_create_signer_with_capability) - [Function `get_signer_capability_address`](#0x1_account_get_signer_capability_address) +- [Function `verify_signed_message`](#0x1_account_verify_signed_message) - [Specification](#@Specification_1) - [Function `initialize`](#@Specification_1_initialize) - [Function `create_account`](#@Specification_1_create_account) @@ -1378,35 +1379,17 @@ to the account owner's signer capability). let source_address = signer::address_of(account); assert!(exists_at(recipient_address), error::not_found(EACCOUNT_DOES_NOT_EXIST)); - let account_resource = borrow_global_mut<Account>(source_address); - // Proof that this account intends to delegate its signer capability to another account. let proof_challenge = SignerCapabilityOfferProofChallengeV2 { - sequence_number: account_resource.sequence_number, + sequence_number: get_sequence_number(source_address), source_address, recipient_address, }; - - // Verify that the `SignerCapabilityOfferProofChallengeV2` has the right information and is signed by the account owner's key - if (account_scheme == ED25519_SCHEME) { - let pubkey = ed25519::new_unvalidated_public_key_from_bytes(account_public_key_bytes); - let expected_auth_key = ed25519::unvalidated_public_key_to_authentication_key(&pubkey); - assert!(account_resource.authentication_key == expected_auth_key, error::invalid_argument(EWRONG_CURRENT_PUBLIC_KEY)); - - let signer_capability_sig = ed25519::new_signature_from_bytes(signer_capability_sig_bytes); - assert!(ed25519::signature_verify_strict_t(&signer_capability_sig, &pubkey, proof_challenge), error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE)); - } else if (account_scheme == MULTI_ED25519_SCHEME) { - let pubkey = multi_ed25519::new_unvalidated_public_key_from_bytes(account_public_key_bytes); - let expected_auth_key = multi_ed25519::unvalidated_public_key_to_authentication_key(&pubkey); - assert!(account_resource.authentication_key == expected_auth_key, error::invalid_argument(EWRONG_CURRENT_PUBLIC_KEY)); - - let signer_capability_sig = multi_ed25519::new_signature_from_bytes(signer_capability_sig_bytes); - assert!(multi_ed25519::signature_verify_strict_t(&signer_capability_sig, &pubkey, proof_challenge), error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE)); - } else { - abort error::invalid_argument(EINVALID_SCHEME) - }; + verify_signed_message( + source_address, account_scheme, account_public_key_bytes, signer_capability_sig_bytes, proof_challenge); // Update the existing signer capability offer or put in a new signer capability offer for the recipient. + let account_resource = borrow_global_mut<Account>(source_address); option::swap_or_fill(&mut account_resource.signer_capability_offer.for, recipient_address); } @@ -1914,6 +1897,66 @@ Capability based functions for efficient use. + + + + +## Function `verify_signed_message` + + + +
public fun verify_signed_message<T: drop>(account: address, account_scheme: u8, account_public_key: vector<u8>, signed_message_bytes: vector<u8>, message: T)
+
+ + + +
+Implementation + + +
public fun verify_signed_message<T: drop>(
+    account: address,
+    account_scheme: u8,
+    account_public_key: vector<u8>,
+    signed_message_bytes: vector<u8>,
+    message: T,
+) acquires Account {
+    let account_resource = borrow_global_mut<Account>(account);
+    // Verify that the `SignerCapabilityOfferProofChallengeV2` has the right information and is signed by the account owner's key
+    if (account_scheme == ED25519_SCHEME) {
+        let pubkey = ed25519::new_unvalidated_public_key_from_bytes(account_public_key);
+        let expected_auth_key = ed25519::unvalidated_public_key_to_authentication_key(&pubkey);
+        assert!(
+            account_resource.authentication_key == expected_auth_key,
+            error::invalid_argument(EWRONG_CURRENT_PUBLIC_KEY),
+        );
+
+        let signer_capability_sig = ed25519::new_signature_from_bytes(signed_message_bytes);
+        assert!(
+            ed25519::signature_verify_strict_t(&signer_capability_sig, &pubkey, message),
+            error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE),
+        );
+    } else if (account_scheme == MULTI_ED25519_SCHEME) {
+        let pubkey = multi_ed25519::new_unvalidated_public_key_from_bytes(account_public_key);
+        let expected_auth_key = multi_ed25519::unvalidated_public_key_to_authentication_key(&pubkey);
+        assert!(
+            account_resource.authentication_key == expected_auth_key,
+            error::invalid_argument(EWRONG_CURRENT_PUBLIC_KEY),
+        );
+
+        let signer_capability_sig = multi_ed25519::new_signature_from_bytes(signed_message_bytes);
+        assert!(
+            multi_ed25519::signature_verify_strict_t(&signer_capability_sig, &pubkey, message),
+            error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE),
+        );
+    } else {
+        abort error::invalid_argument(EINVALID_SCHEME)
+    };
+}
+
+ + +
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 0000000000000..52c475cce021f --- /dev/null +++ b/aptos-move/framework/aptos-framework/doc/multisig_account.md @@ -0,0 +1,2128 @@ + + + +# 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. 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. 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 finalize the rejection by calling execute_rejected_transaction(). + + +- [Resource `MultisigAccount`](#0x1_multisig_account_MultisigAccount) +- [Struct `MultisigTransaction`](#0x1_multisig_account_MultisigTransaction) +- [Struct `ExecutionError`](#0x1_multisig_account_ExecutionError) +- [Resource `OwnedMultisigAccounts`](#0x1_multisig_account_OwnedMultisigAccounts) +- [Struct `MultisigAccountCreationMessage`](#0x1_multisig_account_MultisigAccountCreationMessage) +- [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 `VoteEvent`](#0x1_multisig_account_VoteEvent) +- [Struct `ExecuteRejectedTransactionEvent`](#0x1_multisig_account_ExecuteRejectedTransactionEvent) +- [Struct `TransactionExecutionSucceededEvent`](#0x1_multisig_account_TransactionExecutionSucceededEvent) +- [Struct `TransactionExecutionFailedEvent`](#0x1_multisig_account_TransactionExecutionFailedEvent) +- [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 `get_next_transaction_payload`](#0x1_multisig_account_get_next_transaction_payload) +- [Function `can_be_executed`](#0x1_multisig_account_can_be_executed) +- [Function `can_be_execute_rejected`](#0x1_multisig_account_can_be_execute_rejected) +- [Function `get_next_multisig_account_address`](#0x1_multisig_account_get_next_multisig_account_address) +- [Function `last_resolved_transaction_id`](#0x1_multisig_account_last_resolved_transaction_id) +- [Function `owned_multisig_accounts`](#0x1_multisig_account_owned_multisig_accounts) +- [Function `create_with_existing_account`](#0x1_multisig_account_create_with_existing_account) +- [Function `create`](#0x1_multisig_account_create) +- [Function `create_with_owners`](#0x1_multisig_account_create_with_owners) +- [Function `create_with_owners_internal`](#0x1_multisig_account_create_with_owners_internal) +- [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 `execute_rejected_transaction`](#0x1_multisig_account_execute_rejected_transaction) +- [Function `validate_multisig_transaction`](#0x1_multisig_account_validate_multisig_transaction) +- [Function `successful_transaction_execution_cleanup`](#0x1_multisig_account_successful_transaction_execution_cleanup) +- [Function `failed_transaction_execution_cleanup`](#0x1_multisig_account_failed_transaction_execution_cleanup) +- [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 `num_approvals_and_rejections`](#0x1_multisig_account_num_approvals_and_rejections) +- [Function `assert_multisig_account_exists`](#0x1_multisig_account_assert_multisig_account_exists) +- [Function `add_multisig_account_references`](#0x1_multisig_account_add_multisig_account_references) +- [Function `remove_multisig_account_references`](#0x1_multisig_account_remove_multisig_account_references) + + +
use 0x1::account;
+use 0x1::aptos_coin;
+use 0x1::bcs;
+use 0x1::chain_id;
+use 0x1::coin;
+use 0x1::create_signer;
+use 0x1::error;
+use 0x1::event;
+use 0x1::hash;
+use 0x1::option;
+use 0x1::signer;
+use 0x1::simple_map;
+use 0x1::string;
+use 0x1::table;
+use 0x1::timestamp;
+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_executed_transaction_id: u64 +
+
+ +
+
+next_pending_transaction_id: u64 +
+
+ +
+
+signer_cap: option::Option<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> +
+
+ +
+
+vote_events: event::EventHandle<multisig_account::VoteEvent> +
+
+ +
+
+execute_rejected_transaction_events: event::EventHandle<multisig_account::ExecuteRejectedTransactionEvent> +
+
+ +
+
+execute_transaction_events: event::EventHandle<multisig_account::TransactionExecutionSucceededEvent> +
+
+ +
+
+transaction_execution_failed_events: event::EventHandle<multisig_account::TransactionExecutionFailedEvent> +
+
+ +
+
+ + +
+ + + +## 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<vector<u8>> +
+
+ +
+
+payload_hash: option::Option<vector<u8>> +
+
+ +
+
+votes: simple_map::SimpleMap<address, bool> +
+
+ +
+
+creator: address +
+
+ +
+
+creation_time_secs: u64 +
+
+ +
+
+ + +
+ + + +## Struct `ExecutionError` + +Contains information about execution failure. + + +
struct ExecutionError has copy, drop, store
+
+ + + +
+Fields + + +
+
+abort_location: string::String +
+
+ +
+
+error_type: string::String +
+
+ +
+
+error_code: u64 +
+
+ +
+
+ + +
+ + + +## Resource `OwnedMultisigAccounts` + +A convenient resource to track which multisig accounts a given user account is an owner of. + + +
struct OwnedMultisigAccounts has key
+
+ + + +
+Fields + + +
+
+multisig_accounts: vector<address> +
+
+ +
+
+ + +
+ + + +## Struct `MultisigAccountCreationMessage` + +Used only for verifying multisig account creation on top of existing accounts. + + +
struct MultisigAccountCreationMessage has copy, drop
+
+ + + +
+Fields + + +
+
+chain_id: u8 +
+
+ +
+
+account_address: address +
+
+ +
+
+sequence_number: u64 +
+
+ +
+
+ + +
+ + + +## 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 + + +
+
+creator: address +
+
+ +
+
+transaction_id: u64 +
+
+ +
+
+transaction: multisig_account::MultisigTransaction +
+
+ +
+
+ + +
+ + + +## Struct `VoteEvent` + +Event emitted when an owner approves or rejects a transaction. + + +
struct VoteEvent has drop, store
+
+ + + +
+Fields + + +
+
+owner: address +
+
+ +
+
+transaction_id: u64 +
+
+ +
+
+approved: bool +
+
+ +
+
+ + +
+ + + +## Struct `ExecuteRejectedTransactionEvent` + +Event emitted when a transaction is officially rejected because the number of rejections has reached the +number of signatures required. + + +
struct ExecuteRejectedTransactionEvent has drop, store
+
+ + + +
+Fields + + +
+
+transaction_id: u64 +
+
+ +
+
+num_rejections: u64 +
+
+ +
+
+executor: address +
+
+ +
+
+ + +
+ + + +## Struct `TransactionExecutionSucceededEvent` + +Event emitted when a transaction is executed. + + +
struct TransactionExecutionSucceededEvent has drop, store
+
+ + + +
+Fields + + +
+
+executor: address +
+
+ +
+
+transaction_id: u64 +
+
+ +
+
+transaction_payload: vector<u8> +
+
+ +
+
+num_approvals: u64 +
+
+ +
+
+ + +
+ + + +## Struct `TransactionExecutionFailedEvent` + +Event emitted when a transaction's execution failed. + + +
struct TransactionExecutionFailedEvent has drop, store
+
+ + + +
+Fields + + +
+
+executor: address +
+
+ +
+
+transaction_id: u64 +
+
+ +
+
+transaction_payload: vector<u8> +
+
+ +
+
+num_approvals: u64 +
+
+ +
+
+execution_error: multisig_account::ExecutionError +
+
+ +
+
+ + +
+ + + +## 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 DOMAIN_SEPARATOR: vector<u8> = [97, 112, 116, 111, 115, 95, 102, 114, 97, 109, 101, 119, 111, 114, 107, 58, 58, 109, 117, 108, 116, 105, 115, 105, 103, 95, 97, 99, 99, 111, 117, 110, 116];
+
+ + + + + +Specified account is not a multisig account. + + +
const EACCOUNT_NOT_MULTISIG: u64 = 2002;
+
+ + + + + +Owner list cannot contain the same address more than once. + + +
const EDUPLICATE_OWNER: u64 = 1;
+
+ + + + + +Payload hash must be exactly 32 bytes (sha3-256). + + +
const EINVALID_PAYLOAD_HASH: u64 = 12;
+
+ + + + + +Number of signatures required must be more than zero and at most the total number of owners. + + +
const EINVALID_SIGNATURES_REQUIRED: u64 = 11;
+
+ + + + + +Transaction has not received enough approvals to be executed. + + +
const ENOT_ENOUGH_APPROVALS: u64 = 2009;
+
+ + + + + +Multisig account must have at least one owner. + + +
const ENOT_ENOUGH_OWNERS: u64 = 5;
+
+ + + + + +Transaction has not received enough rejections to be officially rejected. + + +
const ENOT_ENOUGH_REJECTIONS: u64 = 10;
+
+ + + + + +Account executing this operation is not an owner of the multisig account. + + +
const ENOT_OWNER: u64 = 2003;
+
+ + + + + +The multisig account itself cannot be an owner. + + +
const EOWNER_CANNOT_BE_MULTISIG_ACCOUNT_ITSELF: u64 = 13;
+
+ + + + + +Transaction payload cannot be empty. + + +
const EPAYLOAD_CANNOT_BE_EMPTY: u64 = 4;
+
+ + + + + +Provided target function does not match the hash stored in the on-chain transaction. + + +
const EPAYLOAD_DOES_NOT_MATCH_HASH: u64 = 2008;
+
+ + + + + +Cannot execute the specified transaction simply via transaction_id as the full payload is not stored on chain. + + +
const EPAYLOAD_NOT_STORED: u64 = 7;
+
+ + + + + +Transaction with specified id cannot be found. + + +
const ETRANSACTION_NOT_FOUND: u64 = 2006;
+
+ + + + + +## Function `signatures_required` + +Return the number of signatures required to execute or execute-reject a transaction in the provided +multisig account. + + +
public fun signatures_required(multisig_account: address): u64
+
+ + + +
+Implementation + + +
public fun signatures_required(multisig_account: address): u64 acquires MultisigAccount {
+    borrow_global<MultisigAccount>(multisig_account).signatures_required
+}
+
+ + + +
+ + + +## Function `owners` + +Return a vector of all of the provided multisig account's owners. + + +
public fun owners(multisig_account: address): vector<address>
+
+ + + +
+Implementation + + +
public fun owners(multisig_account: address): vector<address> acquires MultisigAccount {
+    borrow_global<MultisigAccount>(multisig_account).owners
+}
+
+ + + +
+ + + +## Function `get_transaction` + +Return the transaction with the given transaction id. + + +
public fun get_transaction(multisig_account: address, transaction_id: u64): multisig_account::MultisigTransaction
+
+ + + +
+Implementation + + +
public fun get_transaction(
+    multisig_account: address,
+    transaction_id: u64,
+): MultisigTransaction acquires MultisigAccount {
+    *table::borrow(&borrow_global<MultisigAccount>(multisig_account).transactions, transaction_id)
+}
+
+ + + +
+ + + +## Function `get_next_transaction_payload` + +Return the payload for the next transaction in the queue. + + +
public fun get_next_transaction_payload(multisig_account: address, provided_payload: vector<u8>): vector<u8>
+
+ + + +
+Implementation + + +
public fun get_next_transaction_payload(
+    multisig_account: address, provided_payload: vector<u8>): vector<u8> acquires MultisigAccount {
+    let multisig_account_resource = borrow_global<MultisigAccount>(multisig_account);
+    let transaction_id = multisig_account_resource.last_executed_transaction_id + 1;
+    let transaction = table::borrow(&multisig_account_resource.transactions, transaction_id);
+
+    if (option::is_some(&transaction.payload)) {
+        *option::borrow(&transaction.payload)
+    } else {
+        provided_payload
+    }
+}
+
+ + + +
+ + + +## Function `can_be_executed` + +Return true if the transaction with given transaction id can be executed now. + + +
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 {
+    let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
+    let transaction = table::borrow(&mut multisig_account_resource.transactions, transaction_id);
+    let (num_approvals, _) = num_approvals_and_rejections(&multisig_account_resource.owners, transaction);
+    transaction_id == multisig_account_resource.last_executed_transaction_id + 1 &&
+        num_approvals >= multisig_account_resource.signatures_required
+}
+
+ + + +
+ + + +## Function `can_be_execute_rejected` + +Return true if the transaction with given transaction id can be officially rejected. + + +
public fun can_be_execute_rejected(multisig_account: address, transaction_id: u64): bool
+
+ + + +
+Implementation + + +
public fun can_be_execute_rejected(
+    multisig_account: address, transaction_id: u64): bool acquires MultisigAccount {
+    let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
+    let transaction = table::borrow(&mut multisig_account_resource.transactions, transaction_id);
+    let (_, num_rejections) = num_approvals_and_rejections(&multisig_account_resource.owners, transaction);
+    transaction_id == multisig_account_resource.last_executed_transaction_id + 1 &&
+        num_rejections >= multisig_account_resource.signatures_required
+}
+
+ + + +
+ + + +## Function `get_next_multisig_account_address` + +Return the predicted address for the next multisig account if created from the given creator address. + + +
public fun get_next_multisig_account_address(creator: address): address
+
+ + + +
+Implementation + + +
public fun get_next_multisig_account_address(creator: address): address {
+    let owner_nonce = account::get_sequence_number(creator);
+    create_resource_address(&creator, create_multisig_account_seed(to_bytes(&owner_nonce)))
+}
+
+ + + +
+ + + +## Function `last_resolved_transaction_id` + +Return the id of the last transaction that was executed (successful or failed) or removed. + + +
public fun last_resolved_transaction_id(multisig_account: address): u64
+
+ + + +
+Implementation + + +
public fun last_resolved_transaction_id(multisig_account: address): u64 acquires MultisigAccount {
+    let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
+    multisig_account_resource.last_executed_transaction_id
+}
+
+ + + +
+ + + +## Function `owned_multisig_accounts` + +Return the list of all the multisig accounts a given address is one of the owners of. + + +
public fun owned_multisig_accounts(owner: address): vector<address>
+
+ + + +
+Implementation + + +
public fun owned_multisig_accounts(owner: address): vector<address> acquires OwnedMultisigAccounts {
+    if (exists<OwnedMultisigAccounts>(owner)) {
+        borrow_global<OwnedMultisigAccounts>(owner).multisig_accounts
+    } else {
+        vector[]
+    }
+}
+
+ + + +
+ + + +## Function `create_with_existing_account` + +Creates a new multisig account on top of an existing account. + +This offers a migration path for an existing account with a multi-ed25519 auth key (native multisig account). +In order to ensure a malicious module cannot obtain backdoor control over an existing account, a signed message +with a valid signature from the account's auth key is required. + + +
public entry fun create_with_existing_account(multisig_account: &signer, owners: vector<address>, signatures_required: u64, account_scheme: u8, account_public_key: vector<u8>, create_multisig_account_signed_message: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun create_with_existing_account(
+    multisig_account: &signer,
+    owners: vector<address>,
+    signatures_required: u64,
+    account_scheme: u8,
+    account_public_key: vector<u8>,
+    create_multisig_account_signed_message: vector<u8>,
+) acquires OwnedMultisigAccounts {
+    let multisig_address = address_of(multisig_account);
+    // Verify that the `MultisigAccountCreationMessage` has the right information and is signed by the account
+    // owner's key.
+    let proof_challenge = MultisigAccountCreationMessage {
+        chain_id: chain_id::get(),
+        account_address: multisig_address,
+        sequence_number: account::get_sequence_number(multisig_address),
+    };
+    account::verify_signed_message(
+        multisig_address,
+        account_scheme,
+        account_public_key,
+        create_multisig_account_signed_message,
+        proof_challenge,
+    );
+
+    create_with_owners_internal(
+        multisig_account,
+        owners,
+        signatures_required,
+        option::none<SignerCapability>(),
+    );
+}
+
+ + + +
+ + + +## Function `create` + +Creates a new multisig account and add the signer as a single owner. + + +
public entry fun create(owner: &signer, signatures_required: u64)
+
+ + + +
+Implementation + + +
public entry fun create(owner: &signer, signatures_required: u64) acquires OwnedMultisigAccounts {
+    create_with_owners(owner, vector[], signatures_required);
+}
+
+ + + +
+ + + +## 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)
+
+ + + +
+Implementation + + +
public entry fun create_with_owners(
+    owner: &signer, additional_owners: vector<address>, signatures_required: u64) acquires OwnedMultisigAccounts {
+    let (multisig_account, multisig_signer_cap) = create_multisig_account(owner);
+    vector::push_back(&mut additional_owners, address_of(owner));
+    create_with_owners_internal(
+        &multisig_account,
+        additional_owners,
+        signatures_required,
+        option::some(multisig_signer_cap),
+    );
+}
+
+ + + +
+ + + +## Function `create_with_owners_internal` + + + +
fun create_with_owners_internal(multisig_account: &signer, owners: vector<address>, signatures_required: u64, multisig_account_signer_cap: option::Option<account::SignerCapability>)
+
+ + + +
+Implementation + + +
fun create_with_owners_internal(
+    multisig_account: &signer,
+    owners: vector<address>,
+    signatures_required: u64,
+    multisig_account_signer_cap: Option<SignerCapability>,
+) acquires OwnedMultisigAccounts {
+    assert!(
+        signatures_required > 0 && signatures_required <= vector::length(&owners),
+        error::invalid_argument(EINVALID_SIGNATURES_REQUIRED),
+    );
+
+    let multisig_address = address_of(multisig_account);
+    validate_owners(&owners, multisig_address);
+    move_to(multisig_account, MultisigAccount {
+        owners,
+        signatures_required,
+        transactions: table::new<u64, MultisigTransaction>(),
+        // First transaction will start at id 1 instead of 0.
+        last_executed_transaction_id: 0,
+        next_pending_transaction_id: 1,
+        signer_cap: multisig_account_signer_cap,
+        add_owners_events: new_event_handle<AddOwnersEvent>(multisig_account),
+        remove_owners_events: new_event_handle<RemoveOwnersEvent>(multisig_account),
+        update_signature_required_events: new_event_handle<UpdateSignaturesRequiredEvent>(multisig_account),
+        create_transaction_events: new_event_handle<CreateTransactionEvent>(multisig_account),
+        vote_events: new_event_handle<VoteEvent>(multisig_account),
+        execute_rejected_transaction_events: new_event_handle<ExecuteRejectedTransactionEvent>(multisig_account),
+        execute_transaction_events: new_event_handle<TransactionExecutionSucceededEvent>(multisig_account),
+        transaction_execution_failed_events: new_event_handle<TransactionExecutionFailedEvent>(multisig_account),
+    });
+
+    add_multisig_account_references(&owners, multisig_address);
+}
+
+ + + +
+ + + +## Function `add_owners` + +Add new owners to the multisig account. This can only be invoked by the multisig account itself, through the +proposal flow. + +Note that this function is not public so it can only be invoked directly instead of via a module or script. This +ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to +maliciously alter the owners list. + + +
entry fun add_owners(multisig_account: &signer, new_owners: vector<address>)
+
+ + + +
+Implementation + + +
entry fun add_owners(
+    multisig_account: &signer, new_owners: vector<address>) acquires MultisigAccount, OwnedMultisigAccounts {
+    // 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);
+    // This will fail if an existing owner is added again.
+    validate_owners(&multisig_account_resource.owners, multisig_address);
+    add_multisig_account_references(&new_owners, multisig_address);
+    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. +Note that this function is not public so it can only be invoked directly instead of via a module or script. This +ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to +maliciously alter the owners list. + + +
entry fun remove_owners(multisig_account: &signer, owners_to_remove: vector<address>)
+
+ + + +
+Implementation + + +
entry fun remove_owners(
+    multisig_account: &signer, owners_to_remove: vector<address>) acquires MultisigAccount, OwnedMultisigAccounts {
+    // 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 as many owners as the number of signatures required.
+    // This also ensures that there's at least one owner left as signature threshold must be > 0.
+    assert!(
+        vector::length(owners) >= multisig_account_resource.signatures_required,
+        error::invalid_state(ENOT_ENOUGH_OWNERS),
+    );
+
+    remove_multisig_account_references(&owners_removed, multisig_address);
+    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. +Note that this function is not public so it can only be invoked directly instead of via a module or script. This +ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to +maliciously alter the number of signatures required. + + +
entry fun update_signatures_required(multisig_account: &signer, new_signatures_required: u64)
+
+ + + +
+Implementation + + +
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);
+    // 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 num_owners = vector::length(&multisig_account_resource.owners);
+    assert!(
+        new_signatures_required > 0 && new_signatures_required <= num_owners,
+        error::invalid_argument(EINVALID_SIGNATURES_REQUIRED),
+    );
+
+    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, payload: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun create_transaction(
+    owner: &signer,
+    multisig_account: address,
+    payload: vector<u8>,
+) acquires MultisigAccount {
+    assert!(vector::length(&payload) > 0, error::invalid_argument(EPAYLOAD_CANNOT_BE_EMPTY));
+
+    assert_multisig_account_exists(multisig_account);
+    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(payload),
+        payload_hash: option::none<vector<u8>>(),
+        votes: simple_map::create<address, bool>(),
+        creator,
+        creation_time_secs: now_seconds(),
+    };
+    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, payload_hash: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun create_transaction_with_hash(
+    owner: &signer,
+    multisig_account: address,
+    payload_hash: vector<u8>,
+) acquires MultisigAccount {
+    // Payload hash is a sha3-256 hash, so it must be exactly 32 bytes.
+    assert!(vector::length(&payload_hash) == 32, error::invalid_argument(EINVALID_PAYLOAD_HASH));
+
+    assert_multisig_account_exists(multisig_account);
+    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<vector<u8>>(),
+        payload_hash: option::some(payload_hash),
+        votes: simple_map::create<address, bool>(),
+        creator,
+        creation_time_secs: now_seconds(),
+    };
+    add_transaction(creator, multisig_account_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 votes = &mut transaction.votes;
+    let owner_addr = address_of(owner);
+
+    if (simple_map::contains_key(votes, &owner_addr)) {
+        *simple_map::borrow_mut(votes, &owner_addr) = approved;
+    } else {
+        simple_map::add(votes, owner_addr, approved);
+    };
+
+    emit_event(
+        &mut multisig_account_resource.vote_events,
+        VoteEvent {
+            owner: owner_addr,
+            transaction_id,
+            approved,
+        }
+    );
+}
+
+ + + +
+ + + +## Function `execute_rejected_transaction` + +Remove the next transaction if it has sufficient owner rejections. + + +
public entry fun execute_rejected_transaction(owner: &signer, multisig_account: address)
+
+ + + +
+Implementation + + +
public entry fun execute_rejected_transaction(
+    owner: &signer,
+    multisig_account: address,
+) acquires MultisigAccount {
+    assert_multisig_account_exists(multisig_account);
+    let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
+    assert_is_owner(owner, multisig_account_resource);
+    let transaction_id = multisig_account_resource.last_executed_transaction_id + 1;
+    assert!(
+        table::contains(&multisig_account_resource.transactions, transaction_id),
+        error::not_found(ETRANSACTION_NOT_FOUND),
+    );
+    // Delete the transaction to keep storage efficient. Off-chain components can reconstruct the transaction and
+    // from events.
+    let transaction = table::remove(&mut multisig_account_resource.transactions, transaction_id);
+    let (_, num_rejections) = num_approvals_and_rejections(&multisig_account_resource.owners, &transaction);
+    assert!(
+        num_rejections >= multisig_account_resource.signatures_required,
+        error::invalid_state(ENOT_ENOUGH_REJECTIONS),
+    );
+
+    multisig_account_resource.last_executed_transaction_id = transaction_id;
+    emit_event(
+        &mut multisig_account_resource.execute_rejected_transaction_events,
+        ExecuteRejectedTransactionEvent {
+            transaction_id,
+            num_rejections,
+            executor: address_of(owner),
+        }
+    );
+}
+
+ + + +
+ + + +## Function `validate_multisig_transaction` + +Called by the VM as part of transaction prologue, which is invoked during mempool transaction validation and as +the first step of transaction execution. + +Transaction payload is optional if it's already stored on chain for the transaction. + + +
fun validate_multisig_transaction(owner: &signer, multisig_account: address, payload: vector<u8>)
+
+ + + +
+Implementation + + +
fun validate_multisig_transaction(
+    owner: &signer, multisig_account: address, payload: vector<u8>) acquires MultisigAccount {
+    assert_multisig_account_exists(multisig_account);
+    let multisig_account_resource = borrow_global<MultisigAccount>(multisig_account);
+    assert_is_owner(owner, multisig_account_resource);
+    let transaction_id = multisig_account_resource.last_executed_transaction_id + 1;
+    assert!(
+        table::contains(&multisig_account_resource.transactions, transaction_id),
+        error::invalid_argument(ETRANSACTION_NOT_FOUND),
+    );
+    let transaction = table::borrow(&multisig_account_resource.transactions, transaction_id);
+    let (num_approvals, _) = num_approvals_and_rejections(&multisig_account_resource.owners, transaction);
+    assert!(
+        num_approvals >= multisig_account_resource.signatures_required,
+        error::invalid_argument(ENOT_ENOUGH_APPROVALS),
+    );
+
+    // If the transaction payload is not stored on chain, verify that the provided payload matches the hashes stored
+    // on chain.
+    if (option::is_some(&transaction.payload_hash)) {
+        let payload_hash = option::borrow(&transaction.payload_hash);
+        assert!(
+            sha3_256(payload) == *payload_hash,
+            error::invalid_argument(EPAYLOAD_DOES_NOT_MATCH_HASH),
+        );
+    };
+}
+
+ + + +
+ + + +## Function `successful_transaction_execution_cleanup` + +Post-execution cleanup for a successful multisig transaction execution. +This function is private so no other code can call this beside the VM itself as part of MultisigTransaction. + + +
fun successful_transaction_execution_cleanup(executor: address, multisig_account: address, transaction_payload: vector<u8>)
+
+ + + +
+Implementation + + +
fun successful_transaction_execution_cleanup(
+    executor: address,
+    multisig_account: address,
+    transaction_payload: vector<u8>,
+) acquires MultisigAccount {
+    let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
+    let transaction_id = multisig_account_resource.last_executed_transaction_id + 1;
+    // Delete the transaction to keep storage efficient. Off-chain components can reconstruct the transaction and
+    // from events.
+    let transaction = table::remove(&mut multisig_account_resource.transactions, transaction_id);
+    let (num_approvals, _) = num_approvals_and_rejections(&multisig_account_resource.owners, &transaction);
+
+    multisig_account_resource.last_executed_transaction_id = transaction_id;
+    emit_event(
+        &mut multisig_account_resource.execute_transaction_events,
+        TransactionExecutionSucceededEvent {
+            transaction_id,
+            transaction_payload,
+            num_approvals,
+            executor,
+        }
+    );
+}
+
+ + + +
+ + + +## Function `failed_transaction_execution_cleanup` + +Post-execution cleanup for a failed multisig transaction execution. +This function is private so no other code can call this beside the VM itself as part of MultisigTransaction. + + +
fun failed_transaction_execution_cleanup(executor: address, multisig_account: address, transaction_payload: vector<u8>, execution_error: multisig_account::ExecutionError)
+
+ + + +
+Implementation + + +
fun failed_transaction_execution_cleanup(
+    executor: address,
+    multisig_account: address,
+    transaction_payload: vector<u8>,
+    execution_error: ExecutionError,
+) acquires MultisigAccount {
+    let multisig_account_resource = borrow_global_mut<MultisigAccount>(multisig_account);
+    let transaction_id = multisig_account_resource.last_executed_transaction_id + 1;
+    // Delete the transaction to keep storage efficient. Off-chain components can reconstruct the transaction and
+    // from events.
+    let transaction = table::remove(&mut multisig_account_resource.transactions, transaction_id);
+    let (num_approvals, _) = num_approvals_and_rejections(&multisig_account_resource.owners, &transaction);
+
+    multisig_account_resource.last_executed_transaction_id = transaction_id;
+    emit_event(
+        &mut multisig_account_resource.transaction_execution_failed_events,
+        TransactionExecutionFailedEvent {
+            executor,
+            transaction_id,
+            transaction_payload,
+            num_approvals,
+            execution_error,
+        }
+    );
+}
+
+ + + +
+ + + +## 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) {
+    // The transaction creator also automatically votes for the transaction.
+    simple_map::add(&mut transaction.votes, creator, true);
+
+    let transaction_id = multisig_account.next_pending_transaction_id;
+    multisig_account.next_pending_transaction_id = transaction_id + 1;
+    table::add(&mut multisig_account.transactions, transaction_id, transaction);
+    emit_event(
+        &mut multisig_account.create_transaction_events,
+        CreateTransactionEvent { creator, transaction_id, transaction },
+    );
+}
+
+ + + +
+ + + +## Function `create_multisig_account` + + + +
fun create_multisig_account(owner: &signer): (signer, account::SignerCapability)
+
+ + + +
+Implementation + + +
fun create_multisig_account(owner: &signer): (signer, SignerCapability) {
+    let owner_nonce = account::get_sequence_number(address_of(owner));
+    let (multisig_signer, multisig_signer_cap) =
+        account::create_resource_account(owner, create_multisig_account_seed(to_bytes(&owner_nonce)));
+    // Register the account to receive APT as this is not done by default as part of the resource account creation
+    // flow.
+    if (!coin::is_account_registered<AptosCoin>(address_of(&multisig_signer))) {
+        coin::register<AptosCoin>(&multisig_signer);
+    };
+
+    (multisig_signer, multisig_signer_cap)
+}
+
+ + + +
+ + + +## 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, DOMAIN_SEPARATOR);
+    vector::append(&mut multisig_account_seed, seed);
+
+    multisig_account_seed
+}
+
+ + + +
+ + + +## Function `validate_owners` + + + +
fun validate_owners(owners: &vector<address>, multisig_account: address)
+
+ + + +
+Implementation + + +
fun validate_owners(owners: &vector<address>, multisig_account: 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!(owner != multisig_account, error::invalid_argument(EOWNER_CANNOT_BE_MULTISIG_ACCOUNT_ITSELF));
+        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 `num_approvals_and_rejections` + + + +
fun num_approvals_and_rejections(owners: &vector<address>, transaction: &multisig_account::MultisigTransaction): (u64, u64)
+
+ + + +
+Implementation + + +
fun num_approvals_and_rejections(owners: &vector<address>, transaction: &MultisigTransaction): (u64, u64) {
+    let num_approvals = 0;
+    let num_rejections = 0;
+
+    let votes = &transaction.votes;
+    let len = vector::length(owners);
+    let i = 0;
+    while (i < len) {
+        let owner = *vector::borrow(owners, i);
+        i = i + 1;
+
+        if (!simple_map::contains_key(votes, &owner)) {
+            continue
+        } else if (*simple_map::borrow(votes, &owner)) {
+            num_approvals = num_approvals + 1;
+        } else {
+            num_rejections = num_rejections + 1;
+        };
+    };
+
+    (num_approvals, num_rejections)
+}
+
+ + + +
+ + + +## 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));
+}
+
+ + + +
+ + + +## Function `add_multisig_account_references` + + + +
fun add_multisig_account_references(owners: &vector<address>, multisig_account: address)
+
+ + + +
+Implementation + + +
fun add_multisig_account_references(
+    owners: &vector<address>, multisig_account: address) acquires OwnedMultisigAccounts {
+    let len = vector::length(owners);
+    let i = 0;
+    while (i < len) {
+        let owner = *vector::borrow(owners, i);
+        if (!exists<OwnedMultisigAccounts>(owner)) {
+            move_to(&create_signer(owner), OwnedMultisigAccounts {
+                multisig_accounts: vector[multisig_account],
+            });
+        } else {
+            let owned_multisig_accounts =
+                &mut borrow_global_mut<OwnedMultisigAccounts>(owner).multisig_accounts;
+            // There should be no duplicate as an owner cannot be added twice to the same multisig account.
+            vector::push_back(owned_multisig_accounts, multisig_account);
+        };
+        i = i + 1;
+    };
+}
+
+ + + +
+ + + +## Function `remove_multisig_account_references` + + + +
fun remove_multisig_account_references(owners: &vector<address>, multisig_account: address)
+
+ + + +
+Implementation + + +
fun remove_multisig_account_references(
+    owners: &vector<address>, multisig_account: address) acquires OwnedMultisigAccounts {
+    let len = vector::length(owners);
+    let i = 0;
+    while (i < len) {
+        let owner = *vector::borrow(owners, i);
+        if (exists<OwnedMultisigAccounts>(owner)) {
+            let owned_multisig_accounts =
+                &mut borrow_global_mut<OwnedMultisigAccounts>(owner).multisig_accounts;
+            let (found, index) = vector::index_of(owned_multisig_accounts, &multisig_account);
+            if (found) {
+                vector::swap_remove(owned_multisig_accounts, index);
+            };
+        };
+        i = i + 1;
+    };
+}
+
+ + + +
+ + +[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 a67d7af478b89..92f4159ff8680 100644 --- a/aptos-move/framework/aptos-framework/doc/overview.md +++ b/aptos-move/framework/aptos-framework/doc/overview.md @@ -31,6 +31,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::object`](object.md#0x1_object) - [`0x1::optional_aggregator`](optional_aggregator.md#0x1_optional_aggregator) - [`0x1::reconfiguration`](reconfiguration.md#0x1_reconfiguration) diff --git a/aptos-move/framework/aptos-framework/doc/voting.md b/aptos-move/framework/aptos-framework/doc/voting.md index e81e69acbf356..5db65c2c28f1f 100644 --- a/aptos-move/framework/aptos-framework/doc/voting.md +++ b/aptos-move/framework/aptos-framework/doc/voting.md @@ -739,8 +739,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/account.move b/aptos-move/framework/aptos-framework/sources/account.move index 11b076398e517..0e623cda0c874 100644 --- a/aptos-move/framework/aptos-framework/sources/account.move +++ b/aptos-move/framework/aptos-framework/sources/account.move @@ -19,6 +19,7 @@ module aptos_framework::account { friend aptos_framework::aptos_account; friend aptos_framework::coin; friend aptos_framework::genesis; + friend aptos_framework::multisig_account; friend aptos_framework::resource_account; friend aptos_framework::transaction_validation; @@ -184,11 +185,6 @@ module aptos_framework::account { create_account_unchecked(new_address) } - #[test_only] - public fun create_account_for_test(new_address: address): signer { - create_account_unchecked(new_address) - } - fun create_account_unchecked(new_address: address): signer { let new_account = create_signer(new_address); let authentication_key = bcs::to_bytes(&new_address); @@ -221,14 +217,17 @@ module aptos_framework::account { new_account } + #[view] public fun exists_at(addr: address): bool { exists(addr) } + #[view] public fun get_guid_next_creation_num(addr: address): u64 acquires Account { borrow_global(addr).guid_creation_num } + #[view] public fun get_sequence_number(addr: address): u64 acquires Account { borrow_global(addr).sequence_number } @@ -244,6 +243,7 @@ module aptos_framework::account { *sequence_number = *sequence_number + 1; } + #[view] public fun get_authentication_key(addr: address): vector acquires Account { *&borrow_global(addr).authentication_key } @@ -455,35 +455,17 @@ module aptos_framework::account { let source_address = signer::address_of(account); assert!(exists_at(recipient_address), error::not_found(EACCOUNT_DOES_NOT_EXIST)); - let account_resource = borrow_global_mut(source_address); - // Proof that this account intends to delegate its signer capability to another account. let proof_challenge = SignerCapabilityOfferProofChallengeV2 { - sequence_number: account_resource.sequence_number, + sequence_number: get_sequence_number(source_address), source_address, recipient_address, }; - - // Verify that the `SignerCapabilityOfferProofChallengeV2` has the right information and is signed by the account owner's key - if (account_scheme == ED25519_SCHEME) { - let pubkey = ed25519::new_unvalidated_public_key_from_bytes(account_public_key_bytes); - let expected_auth_key = ed25519::unvalidated_public_key_to_authentication_key(&pubkey); - assert!(account_resource.authentication_key == expected_auth_key, error::invalid_argument(EWRONG_CURRENT_PUBLIC_KEY)); - - let signer_capability_sig = ed25519::new_signature_from_bytes(signer_capability_sig_bytes); - assert!(ed25519::signature_verify_strict_t(&signer_capability_sig, &pubkey, proof_challenge), error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE)); - } else if (account_scheme == MULTI_ED25519_SCHEME) { - let pubkey = multi_ed25519::new_unvalidated_public_key_from_bytes(account_public_key_bytes); - let expected_auth_key = multi_ed25519::unvalidated_public_key_to_authentication_key(&pubkey); - assert!(account_resource.authentication_key == expected_auth_key, error::invalid_argument(EWRONG_CURRENT_PUBLIC_KEY)); - - let signer_capability_sig = multi_ed25519::new_signature_from_bytes(signer_capability_sig_bytes); - assert!(multi_ed25519::signature_verify_strict_t(&signer_capability_sig, &pubkey, proof_challenge), error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE)); - } else { - abort error::invalid_argument(EINVALID_SCHEME) - }; + verify_signed_message( + source_address, account_scheme, account_public_key_bytes, signer_capability_sig_bytes, proof_challenge); // Update the existing signer capability offer or put in a new signer capability offer for the recipient. + let account_resource = borrow_global_mut(source_address); option::swap_or_fill(&mut account_resource.signer_capability_offer.for, recipient_address); } @@ -707,6 +689,51 @@ module aptos_framework::account { capability.account } + public fun verify_signed_message( + account: address, + account_scheme: u8, + account_public_key: vector, + signed_message_bytes: vector, + message: T, + ) acquires Account { + let account_resource = borrow_global_mut(account); + // Verify that the `SignerCapabilityOfferProofChallengeV2` has the right information and is signed by the account owner's key + if (account_scheme == ED25519_SCHEME) { + let pubkey = ed25519::new_unvalidated_public_key_from_bytes(account_public_key); + let expected_auth_key = ed25519::unvalidated_public_key_to_authentication_key(&pubkey); + assert!( + account_resource.authentication_key == expected_auth_key, + error::invalid_argument(EWRONG_CURRENT_PUBLIC_KEY), + ); + + let signer_capability_sig = ed25519::new_signature_from_bytes(signed_message_bytes); + assert!( + ed25519::signature_verify_strict_t(&signer_capability_sig, &pubkey, message), + error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE), + ); + } else if (account_scheme == MULTI_ED25519_SCHEME) { + let pubkey = multi_ed25519::new_unvalidated_public_key_from_bytes(account_public_key); + let expected_auth_key = multi_ed25519::unvalidated_public_key_to_authentication_key(&pubkey); + assert!( + account_resource.authentication_key == expected_auth_key, + error::invalid_argument(EWRONG_CURRENT_PUBLIC_KEY), + ); + + let signer_capability_sig = multi_ed25519::new_signature_from_bytes(signed_message_bytes); + assert!( + multi_ed25519::signature_verify_strict_t(&signer_capability_sig, &pubkey, message), + error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE), + ); + } else { + abort error::invalid_argument(EINVALID_SCHEME) + }; + } + + #[test_only] + public fun create_account_for_test(new_address: address): signer { + create_account_unchecked(new_address) + } + #[test] /// Assert correct signer creation. fun test_create_signer_for_test() { diff --git a/aptos-move/framework/aptos-framework/sources/create_signer.move b/aptos-move/framework/aptos-framework/sources/create_signer.move index 4acec0d9923a7..154c68b32ac54 100644 --- a/aptos-move/framework/aptos-framework/sources/create_signer.move +++ b/aptos-move/framework/aptos-framework/sources/create_signer.move @@ -12,6 +12,7 @@ module aptos_framework::create_signer { friend aptos_framework::account; friend aptos_framework::aptos_account; friend aptos_framework::genesis; + friend aptos_framework::multisig_account; friend aptos_framework::object; public(friend) native fun create_signer(addr: address): signer; 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 0000000000000..4a81faa074bf0 --- /dev/null +++ b/aptos-move/framework/aptos-framework/sources/multisig_account.move @@ -0,0 +1,1438 @@ +/// 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. 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. 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 finalize the rejection by calling execute_rejected_transaction(). +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::chain_id; + use aptos_framework::create_signer::create_signer; + 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 aptos_framework::timestamp::now_seconds; + use std::bcs::to_bytes; + use std::error; + use std::hash::sha3_256; + use std::option::{Self, Option}; + use std::signer::address_of; + use std::string::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 DOMAIN_SEPARATOR: vector = b"aptos_framework::multisig_account"; + + // Any error codes > 2000 can be thrown as part of transaction prologue. + /// 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 = 2002; + /// Account executing this operation is not an owner of the multisig account. + const ENOT_OWNER: u64 = 2003; + /// Transaction payload cannot be empty. + const EPAYLOAD_CANNOT_BE_EMPTY: u64 = 4; + /// Multisig account must have at least one owner. + const ENOT_ENOUGH_OWNERS: u64 = 5; + /// Transaction with specified id cannot be found. + const ETRANSACTION_NOT_FOUND: u64 = 2006; + /// Cannot execute the specified transaction simply via transaction_id as the full payload is not stored on chain. + const EPAYLOAD_NOT_STORED: u64 = 7; + /// Provided target function does not match the hash stored in the on-chain transaction. + const EPAYLOAD_DOES_NOT_MATCH_HASH: u64 = 2008; + /// Transaction has not received enough approvals to be executed. + const ENOT_ENOUGH_APPROVALS: u64 = 2009; + /// Transaction has not received enough rejections to be officially rejected. + const ENOT_ENOUGH_REJECTIONS: u64 = 10; + /// Number of signatures required must be more than zero and at most the total number of owners. + const EINVALID_SIGNATURES_REQUIRED: u64 = 11; + /// Payload hash must be exactly 32 bytes (sha3-256). + const EINVALID_PAYLOAD_HASH: u64 = 12; + /// The multisig account itself cannot be an owner. + const EOWNER_CANNOT_BE_MULTISIG_ACCOUNT_ITSELF: u64 = 13; + + /// 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_executed_transaction_id: u64, + // The transaction id to assign to the next transaction. This is not always last_executed_transaction_id + 1 as there + // can be multiple pending transactions. The number of pending transactions should be equal to + // next_pending_transaction_id - (last_executed_transaction_id + 1). + next_pending_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: Option, + + // Events. + add_owners_events: EventHandle, + remove_owners_events: EventHandle, + update_signature_required_events: EventHandle, + create_transaction_events: EventHandle, + vote_events: EventHandle, + execute_rejected_transaction_events: EventHandle, + execute_transaction_events: EventHandle, + transaction_execution_failed_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>, + // Mapping from owner adress to vote (yes for approve, no for reject). Uses a simple map to deduplicate. + votes: SimpleMap, + // The owner who created this transaction. + creator: address, + // The timestamp in seconds when the transaction was created. + creation_time_secs: u64, + } + + /// Contains information about execution failure. + struct ExecutionError has copy, drop, store { + abort_location: String, + // There are 3 error types, stored as strings: + // 1. VMError. Indicates an error from the VM, e.g. out of gas, invalid auth key, etc. + // 2. MoveAbort. Indicates an abort, e.g. assertion failure, from inside the executed Move code. + // 3. MoveExecutionFailure. Indicates an error from Move code where the VM could not continue. For example, + // arithmetic failures. + error_type: String, + error_code: u64, + } + + /// A convenient resource to track which multisig accounts a given user account is an owner of. + struct OwnedMultisigAccounts has key { + multisig_accounts: vector
, + } + + /// Used only for verifying multisig account creation on top of existing accounts. + struct MultisigAccountCreationMessage has copy, drop { + // Chain id is included to prevent cross-chain replay. + chain_id: u8, + // Account address is included to prevent cross-account replay (when multiple accounts share the same auth key). + account_address: address, + // Sequence number is not needed for replay protection as the multisig account can only be created once. + // But it's included to ensure timely execution of account creation. + sequence_number: u64, + } + + /// 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 { + creator: address, + transaction_id: u64, + transaction: MultisigTransaction, + } + + /// Event emitted when an owner approves or rejects a transaction. + struct VoteEvent has drop, store { + owner: address, + transaction_id: u64, + approved: bool, + } + + /// Event emitted when a transaction is officially rejected because the number of rejections has reached the + /// number of signatures required. + struct ExecuteRejectedTransactionEvent has drop, store { + transaction_id: u64, + num_rejections: u64, + executor: address, + } + + /// Event emitted when a transaction is executed. + struct TransactionExecutionSucceededEvent has drop, store { + executor: address, + transaction_id: u64, + transaction_payload: vector, + num_approvals: u64, + } + + /// Event emitted when a transaction's execution failed. + struct TransactionExecutionFailedEvent has drop, store { + executor: address, + transaction_id: u64, + transaction_payload: vector, + num_approvals: u64, + execution_error: ExecutionError, + } + + #[view] + /// Return the number of signatures required to execute or execute-reject a transaction in the provided + /// multisig account. + public fun signatures_required(multisig_account: address): u64 acquires MultisigAccount { + borrow_global(multisig_account).signatures_required + } + + #[view] + /// Return a vector of all of the provided multisig account's owners. + public fun owners(multisig_account: address): vector
acquires MultisigAccount { + borrow_global(multisig_account).owners + } + + #[view] + /// Return the transaction with the given transaction id. + public fun get_transaction( + multisig_account: address, + transaction_id: u64, + ): MultisigTransaction acquires MultisigAccount { + *table::borrow(&borrow_global(multisig_account).transactions, transaction_id) + } + + #[view] + /// Return the payload for the next transaction in the queue. + public fun get_next_transaction_payload( + multisig_account: address, provided_payload: vector): vector acquires MultisigAccount { + let multisig_account_resource = borrow_global(multisig_account); + let transaction_id = multisig_account_resource.last_executed_transaction_id + 1; + let transaction = table::borrow(&multisig_account_resource.transactions, transaction_id); + + if (option::is_some(&transaction.payload)) { + *option::borrow(&transaction.payload) + } else { + provided_payload + } + } + + #[view] + /// Return true if the transaction with given transaction id can be executed now. + public fun can_be_executed( + multisig_account: address, transaction_id: u64): bool acquires MultisigAccount { + let multisig_account_resource = borrow_global_mut(multisig_account); + let transaction = table::borrow(&mut multisig_account_resource.transactions, transaction_id); + let (num_approvals, _) = num_approvals_and_rejections(&multisig_account_resource.owners, transaction); + transaction_id == multisig_account_resource.last_executed_transaction_id + 1 && + num_approvals >= multisig_account_resource.signatures_required + } + + #[view] + /// Return true if the transaction with given transaction id can be officially rejected. + public fun can_be_execute_rejected( + multisig_account: address, transaction_id: u64): bool acquires MultisigAccount { + let multisig_account_resource = borrow_global_mut(multisig_account); + let transaction = table::borrow(&mut multisig_account_resource.transactions, transaction_id); + let (_, num_rejections) = num_approvals_and_rejections(&multisig_account_resource.owners, transaction); + transaction_id == multisig_account_resource.last_executed_transaction_id + 1 && + num_rejections >= multisig_account_resource.signatures_required + } + + #[view] + /// Return the predicted address for the next multisig account if created from the given creator address. + public fun get_next_multisig_account_address(creator: address): address { + let owner_nonce = account::get_sequence_number(creator); + create_resource_address(&creator, create_multisig_account_seed(to_bytes(&owner_nonce))) + } + + #[view] + /// Return the id of the last transaction that was executed (successful or failed) or removed. + public fun last_resolved_transaction_id(multisig_account: address): u64 acquires MultisigAccount { + let multisig_account_resource = borrow_global_mut(multisig_account); + multisig_account_resource.last_executed_transaction_id + } + + #[view] + /// Return the list of all the multisig accounts a given address is one of the owners of. + public fun owned_multisig_accounts(owner: address): vector
acquires OwnedMultisigAccounts { + if (exists(owner)) { + borrow_global(owner).multisig_accounts + } else { + vector[] + } + } + + /// Creates a new multisig account on top of an existing account. + /// + /// This offers a migration path for an existing account with a multi-ed25519 auth key (native multisig account). + /// In order to ensure a malicious module cannot obtain backdoor control over an existing account, a signed message + /// with a valid signature from the account's auth key is required. + public entry fun create_with_existing_account( + multisig_account: &signer, + owners: vector
, + signatures_required: u64, + account_scheme: u8, + account_public_key: vector, + create_multisig_account_signed_message: vector, + ) acquires OwnedMultisigAccounts { + let multisig_address = address_of(multisig_account); + // Verify that the `MultisigAccountCreationMessage` has the right information and is signed by the account + // owner's key. + let proof_challenge = MultisigAccountCreationMessage { + chain_id: chain_id::get(), + account_address: multisig_address, + sequence_number: account::get_sequence_number(multisig_address), + }; + account::verify_signed_message( + multisig_address, + account_scheme, + account_public_key, + create_multisig_account_signed_message, + proof_challenge, + ); + + create_with_owners_internal( + multisig_account, + owners, + signatures_required, + option::none(), + ); + } + + /// Creates a new multisig account and add the signer as a single owner. + public entry fun create(owner: &signer, signatures_required: u64) acquires OwnedMultisigAccounts { + create_with_owners(owner, vector[], signatures_required); + } + + /// 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) acquires OwnedMultisigAccounts { + let (multisig_account, multisig_signer_cap) = create_multisig_account(owner); + vector::push_back(&mut additional_owners, address_of(owner)); + create_with_owners_internal( + &multisig_account, + additional_owners, + signatures_required, + option::some(multisig_signer_cap), + ); + } + + fun create_with_owners_internal( + multisig_account: &signer, + owners: vector
, + signatures_required: u64, + multisig_account_signer_cap: Option, + ) acquires OwnedMultisigAccounts { + assert!( + signatures_required > 0 && signatures_required <= vector::length(&owners), + error::invalid_argument(EINVALID_SIGNATURES_REQUIRED), + ); + + let multisig_address = address_of(multisig_account); + validate_owners(&owners, multisig_address); + move_to(multisig_account, MultisigAccount { + owners, + signatures_required, + transactions: table::new(), + // First transaction will start at id 1 instead of 0. + last_executed_transaction_id: 0, + next_pending_transaction_id: 1, + signer_cap: multisig_account_signer_cap, + add_owners_events: new_event_handle(multisig_account), + remove_owners_events: new_event_handle(multisig_account), + update_signature_required_events: new_event_handle(multisig_account), + create_transaction_events: new_event_handle(multisig_account), + vote_events: new_event_handle(multisig_account), + execute_rejected_transaction_events: new_event_handle(multisig_account), + execute_transaction_events: new_event_handle(multisig_account), + transaction_execution_failed_events: new_event_handle(multisig_account), + }); + + add_multisig_account_references(&owners, multisig_address); + } + + /// Add new owners to the multisig account. This can only be invoked by the multisig account itself, through the + /// proposal flow. + /// + /// Note that this function is not public so it can only be invoked directly instead of via a module or script. This + /// ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to + /// maliciously alter the owners list. + entry fun add_owners( + multisig_account: &signer, new_owners: vector
) acquires MultisigAccount, OwnedMultisigAccounts { + // 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); + // This will fail if an existing owner is added again. + validate_owners(&multisig_account_resource.owners, multisig_address); + add_multisig_account_references(&new_owners, multisig_address); + 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. + /// Note that this function is not public so it can only be invoked directly instead of via a module or script. This + /// ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to + /// maliciously alter the owners list. + entry fun remove_owners( + multisig_account: &signer, owners_to_remove: vector
) acquires MultisigAccount, OwnedMultisigAccounts { + // 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 as many owners as the number of signatures required. + // This also ensures that there's at least one owner left as signature threshold must be > 0. + assert!( + vector::length(owners) >= multisig_account_resource.signatures_required, + error::invalid_state(ENOT_ENOUGH_OWNERS), + ); + + remove_multisig_account_references(&owners_removed, multisig_address); + 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. + /// Note that this function is not public so it can only be invoked directly instead of via a module or script. This + /// ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to + /// maliciously alter the number of signatures required. + entry fun update_signatures_required( + multisig_account: &signer, new_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); + // 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 num_owners = vector::length(&multisig_account_resource.owners); + assert!( + new_signatures_required > 0 && new_signatures_required <= num_owners, + error::invalid_argument(EINVALID_SIGNATURES_REQUIRED), + ); + + 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, + payload: vector, + ) acquires MultisigAccount { + assert!(vector::length(&payload) > 0, error::invalid_argument(EPAYLOAD_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(payload), + payload_hash: option::none>(), + votes: simple_map::create(), + creator, + creation_time_secs: now_seconds(), + }; + 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, + payload_hash: vector, + ) acquires MultisigAccount { + // Payload hash is a sha3-256 hash, so it must be exactly 32 bytes. + assert!(vector::length(&payload_hash) == 32, error::invalid_argument(EINVALID_PAYLOAD_HASH)); + + assert_multisig_account_exists(multisig_account); + 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(payload_hash), + votes: simple_map::create(), + creator, + creation_time_secs: now_seconds(), + }; + 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 votes = &mut transaction.votes; + let owner_addr = address_of(owner); + + if (simple_map::contains_key(votes, &owner_addr)) { + *simple_map::borrow_mut(votes, &owner_addr) = approved; + } else { + simple_map::add(votes, owner_addr, approved); + }; + + emit_event( + &mut multisig_account_resource.vote_events, + VoteEvent { + owner: owner_addr, + transaction_id, + approved, + } + ); + } + + /// Remove the next transaction if it has sufficient owner rejections. + public entry fun execute_rejected_transaction( + owner: &signer, + multisig_account: address, + ) acquires MultisigAccount { + assert_multisig_account_exists(multisig_account); + let multisig_account_resource = borrow_global_mut(multisig_account); + assert_is_owner(owner, multisig_account_resource); + let transaction_id = multisig_account_resource.last_executed_transaction_id + 1; + assert!( + table::contains(&multisig_account_resource.transactions, transaction_id), + error::not_found(ETRANSACTION_NOT_FOUND), + ); + // Delete the transaction to keep storage efficient. Off-chain components can reconstruct the transaction and + // from events. + let transaction = table::remove(&mut multisig_account_resource.transactions, transaction_id); + let (_, num_rejections) = num_approvals_and_rejections(&multisig_account_resource.owners, &transaction); + assert!( + num_rejections >= multisig_account_resource.signatures_required, + error::invalid_state(ENOT_ENOUGH_REJECTIONS), + ); + + multisig_account_resource.last_executed_transaction_id = transaction_id; + emit_event( + &mut multisig_account_resource.execute_rejected_transaction_events, + ExecuteRejectedTransactionEvent { + transaction_id, + num_rejections, + executor: address_of(owner), + } + ); + } + + /// Called by the VM as part of transaction prologue, which is invoked during mempool transaction validation and as + /// the first step of transaction execution. + /// + /// Transaction payload is optional if it's already stored on chain for the transaction. + fun validate_multisig_transaction( + owner: &signer, multisig_account: address, payload: vector) acquires MultisigAccount { + assert_multisig_account_exists(multisig_account); + let multisig_account_resource = borrow_global(multisig_account); + assert_is_owner(owner, multisig_account_resource); + let transaction_id = multisig_account_resource.last_executed_transaction_id + 1; + assert!( + table::contains(&multisig_account_resource.transactions, transaction_id), + error::invalid_argument(ETRANSACTION_NOT_FOUND), + ); + let transaction = table::borrow(&multisig_account_resource.transactions, transaction_id); + let (num_approvals, _) = num_approvals_and_rejections(&multisig_account_resource.owners, transaction); + assert!( + num_approvals >= multisig_account_resource.signatures_required, + error::invalid_argument(ENOT_ENOUGH_APPROVALS), + ); + + // If the transaction payload is not stored on chain, verify that the provided payload matches the hashes stored + // on chain. + if (option::is_some(&transaction.payload_hash)) { + let payload_hash = option::borrow(&transaction.payload_hash); + assert!( + sha3_256(payload) == *payload_hash, + error::invalid_argument(EPAYLOAD_DOES_NOT_MATCH_HASH), + ); + }; + } + + /// Post-execution cleanup for a successful multisig transaction execution. + /// This function is private so no other code can call this beside the VM itself as part of MultisigTransaction. + fun successful_transaction_execution_cleanup( + executor: address, + multisig_account: address, + transaction_payload: vector, + ) acquires MultisigAccount { + let multisig_account_resource = borrow_global_mut(multisig_account); + let transaction_id = multisig_account_resource.last_executed_transaction_id + 1; + // Delete the transaction to keep storage efficient. Off-chain components can reconstruct the transaction and + // from events. + let transaction = table::remove(&mut multisig_account_resource.transactions, transaction_id); + let (num_approvals, _) = num_approvals_and_rejections(&multisig_account_resource.owners, &transaction); + + multisig_account_resource.last_executed_transaction_id = transaction_id; + emit_event( + &mut multisig_account_resource.execute_transaction_events, + TransactionExecutionSucceededEvent { + transaction_id, + transaction_payload, + num_approvals, + executor, + } + ); + } + + /// Post-execution cleanup for a failed multisig transaction execution. + /// This function is private so no other code can call this beside the VM itself as part of MultisigTransaction. + fun failed_transaction_execution_cleanup( + executor: address, + multisig_account: address, + transaction_payload: vector, + execution_error: ExecutionError, + ) acquires MultisigAccount { + let multisig_account_resource = borrow_global_mut(multisig_account); + let transaction_id = multisig_account_resource.last_executed_transaction_id + 1; + // Delete the transaction to keep storage efficient. Off-chain components can reconstruct the transaction and + // from events. + let transaction = table::remove(&mut multisig_account_resource.transactions, transaction_id); + let (num_approvals, _) = num_approvals_and_rejections(&multisig_account_resource.owners, &transaction); + + multisig_account_resource.last_executed_transaction_id = transaction_id; + emit_event( + &mut multisig_account_resource.transaction_execution_failed_events, + TransactionExecutionFailedEvent { + executor, + transaction_id, + transaction_payload, + num_approvals, + execution_error, + } + ); + } + + fun add_transaction(creator: address, multisig_account: &mut MultisigAccount, transaction: MultisigTransaction) { + // The transaction creator also automatically votes for the transaction. + simple_map::add(&mut transaction.votes, creator, true); + + let transaction_id = multisig_account.next_pending_transaction_id; + multisig_account.next_pending_transaction_id = transaction_id + 1; + table::add(&mut multisig_account.transactions, transaction_id, transaction); + emit_event( + &mut multisig_account.create_transaction_events, + CreateTransactionEvent { creator, transaction_id, transaction }, + ); + } + + fun create_multisig_account(owner: &signer): (signer, SignerCapability) { + let owner_nonce = account::get_sequence_number(address_of(owner)); + let (multisig_signer, multisig_signer_cap) = + account::create_resource_account(owner, create_multisig_account_seed(to_bytes(&owner_nonce))); + // Register the account to receive APT as this is not done by default as part of the resource account creation + // flow. + if (!coin::is_account_registered(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, DOMAIN_SEPARATOR); + vector::append(&mut multisig_account_seed, seed); + + multisig_account_seed + } + + fun validate_owners(owners: &vector
, multisig_account: address) { + let distinct_owners = simple_map::create(); + let i = 0; + let len = vector::length(owners); + while (i < len) { + let owner = *vector::borrow(owners, i); + assert!(owner != multisig_account, error::invalid_argument(EOWNER_CANNOT_BE_MULTISIG_ACCOUNT_ITSELF)); + 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 num_approvals_and_rejections(owners: &vector
, transaction: &MultisigTransaction): (u64, u64) { + let num_approvals = 0; + let num_rejections = 0; + + let votes = &transaction.votes; + let len = vector::length(owners); + let i = 0; + while (i < len) { + let owner = *vector::borrow(owners, i); + i = i + 1; + + if (!simple_map::contains_key(votes, &owner)) { + continue + } else if (*simple_map::borrow(votes, &owner)) { + num_approvals = num_approvals + 1; + } else { + num_rejections = num_rejections + 1; + }; + }; + + (num_approvals, num_rejections) + } + + fun assert_multisig_account_exists(multisig_account: address) { + assert!(exists(multisig_account), error::invalid_state(EACCOUNT_NOT_MULTISIG)); + } + + fun add_multisig_account_references( + owners: &vector
, multisig_account: address) acquires OwnedMultisigAccounts { + let len = vector::length(owners); + let i = 0; + while (i < len) { + let owner = *vector::borrow(owners, i); + if (!exists(owner)) { + move_to(&create_signer(owner), OwnedMultisigAccounts { + multisig_accounts: vector[multisig_account], + }); + } else { + let owned_multisig_accounts = + &mut borrow_global_mut(owner).multisig_accounts; + // There should be no duplicate as an owner cannot be added twice to the same multisig account. + vector::push_back(owned_multisig_accounts, multisig_account); + }; + i = i + 1; + }; + } + + fun remove_multisig_account_references( + owners: &vector
, multisig_account: address) acquires OwnedMultisigAccounts { + let len = vector::length(owners); + let i = 0; + while (i < len) { + let owner = *vector::borrow(owners, i); + if (exists(owner)) { + let owned_multisig_accounts = + &mut borrow_global_mut(owner).multisig_accounts; + let (found, index) = vector::index_of(owned_multisig_accounts, &multisig_account); + if (found) { + vector::swap_remove(owned_multisig_accounts, index); + }; + }; + i = i + 1; + }; + } + + #[test_only] + use aptos_framework::aptos_account::create_account; + #[test_only] + use aptos_framework::timestamp; + #[test_only] + use aptos_std::from_bcs; + #[test_only] + use aptos_std::multi_ed25519; + #[test_only] + use std::string::utf8; + + #[test_only] + const PAYLOAD: vector = vector[1, 2, 3]; + #[test_only] + const ERROR_TYPE: vector = b"MoveAbort"; + #[test_only] + const ABORT_LOCATION: vector = b"abort_location"; + #[test_only] + const ERROR_CODE: u64 = 10; + + #[test_only] + fun execution_error(): ExecutionError { + ExecutionError { + abort_location: utf8(ABORT_LOCATION), + error_type: utf8(ERROR_TYPE), + error_code: ERROR_CODE, + } + } + + #[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, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + 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_account(owner_1_addr); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2); + assert!(owned_multisig_accounts(owner_1_addr) == vector[multisig_account], 0); + assert!(owned_multisig_accounts(owner_2_addr) == vector[multisig_account], 1); + assert!(owned_multisig_accounts(owner_3_addr) == vector[multisig_account], 2); + + // Create three transactions. + create_transaction(owner_1, multisig_account, PAYLOAD); + create_transaction(owner_2, multisig_account, PAYLOAD); + create_transaction_with_hash(owner_3, multisig_account, sha3_256(PAYLOAD)); + + // 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); + // First transaction was executed successfully. + successful_transaction_execution_cleanup(owner_2_addr, multisig_account,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_execute_rejected(multisig_account, 2), 2); + execute_rejected_transaction(owner_1, multisig_account); + + // Third transaction can be executed now but execution fails. + failed_transaction_execution_cleanup(owner_3_addr, multisig_account, PAYLOAD, execution_error()); + } + + #[test(owner = @0x123)] + public entry fun test_create_with_single_owner(owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + let owner_addr = address_of(owner); + create_account(owner_addr); + create(owner, 1); + let multisig_account = get_next_multisig_account_address(owner_addr); + assert_multisig_account_exists(multisig_account); + assert!(owners(multisig_account) == vector[owner_addr], 0); + assert!(owned_multisig_accounts(owner_addr) == vector[multisig_account], 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) acquires OwnedMultisigAccounts { + let owner_1_addr = address_of(owner_1); + create_account(owner_1_addr); + create_with_owners(owner_1, vector[address_of(owner_2), address_of(owner_3)], 3); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + assert_multisig_account_exists(multisig_account); + } + + #[test(owner = @0x123)] + #[expected_failure(abort_code = 0x1000B, location = Self)] + public entry fun test_create_with_zero_signatures_required_should_fail( + owner: &signer) acquires OwnedMultisigAccounts { + create_account(address_of(owner)); + create(owner, 0); + } + + #[test(owner = @0x123)] + #[expected_failure(abort_code = 0x1000B, location = Self)] + public entry fun test_create_with_too_many_signatures_required_should_fail( + owner: &signer) acquires OwnedMultisigAccounts { + create_account(address_of(owner)); + create(owner, 2); + } + + #[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) acquires OwnedMultisigAccounts { + create_account(address_of(owner_1)); + create_with_owners(owner_1, vector[ + // Duplicate owner 2 addresses. + address_of(owner_2), + address_of(owner_3), + address_of(owner_2), + ], 2); + } + + #[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) acquires OwnedMultisigAccounts { + create_account(address_of(owner_1)); + create_with_owners(owner_1, vector[ + // Duplicate owner 1 addresses. + address_of(owner_1), + address_of(owner_2), + address_of(owner_3), + ], 2); + } + + #[test(aptos_framework = @aptos_framework)] + public entry fun test_create_multisig_account_on_top_of_existing_multi_ed25519_account( + aptos_framework: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + chain_id::initialize_for_test(aptos_framework, 1); + account::initialize(aptos_framework); + let (curr_sk, curr_pk) = multi_ed25519::generate_keys(2, 3); + let pk_unvalidated = multi_ed25519::public_key_to_unvalidated(&curr_pk); + let auth_key = multi_ed25519::unvalidated_public_key_to_authentication_key(&pk_unvalidated); + let multisig_address = from_bcs::to_address(auth_key); + let multisig_account = account::create_account_for_test(multisig_address); + + let proof = MultisigAccountCreationMessage { + chain_id: chain_id::get(), + account_address: multisig_address, + sequence_number: account::get_sequence_number(multisig_address), + }; + let signed_proof = multi_ed25519::sign_struct(&curr_sk, proof); + let owners = vector[@0x123, @0x124, @0x125]; + create_with_existing_account( + &multisig_account, + owners, + 2, + 1, // MULTI_ED25519_SCHEME + multi_ed25519::unvalidated_public_key_to_bytes(&pk_unvalidated), + multi_ed25519::signature_to_bytes(&signed_proof), + ); + assert_multisig_account_exists(multisig_address); + assert!(owners(multisig_address) == owners, 0); + } + + #[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, OwnedMultisigAccounts { + let owner_1_addr = address_of(owner_1); + create_account(owner_1_addr); + create_with_owners(owner_1, vector[address_of(owner_2), address_of(owner_3)], 1); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + assert!(signatures_required(multisig_account) == 1, 0); + update_signatures_required(&create_signer(multisig_account), 2); + assert!(signatures_required(multisig_account) == 2, 1); + // As many signatures required as number of owners (3). + update_signatures_required(&create_signer(multisig_account), 3); + assert!(signatures_required(multisig_account) == 3, 2); + } + + #[test(owner = @0x123)] + #[expected_failure(abort_code = 0x1000B, location = Self)] + public entry fun test_update_with_zero_signatures_required_should_fail( + owner:& signer) acquires MultisigAccount, OwnedMultisigAccounts { + create_account(address_of(owner)); + create(owner,1); + let multisig_account = get_next_multisig_account_address(address_of(owner)); + update_signatures_required(&create_signer(multisig_account), 0); + } + + #[test(owner = @0x123)] + #[expected_failure(abort_code = 0x1000B, location = Self)] + public entry fun test_update_with_too_many_signatures_required_should_fail( + owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + create_account(address_of(owner)); + create(owner,1); + let multisig_account = get_next_multisig_account_address(address_of(owner)); + update_signatures_required(&create_signer(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, OwnedMultisigAccounts { + create_account(address_of(owner_1)); + create(owner_1, 1); + 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_next_multisig_account_address(owner_1_addr); + let multisig_signer = &create_signer(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); + assert!(owned_multisig_accounts(owner_2_addr) == vector[multisig_account], 0); + assert!(owned_multisig_accounts(owner_3_addr) == vector[multisig_account], 0); + } + + #[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, OwnedMultisigAccounts { + 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_account(owner_1_addr); + create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 1); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + let multisig_signer = &create_signer(multisig_account); + assert!(owners(multisig_account) == vector[owner_2_addr, owner_3_addr, owner_1_addr], 0); + assert!(owned_multisig_accounts(owner_3_addr) == vector[multisig_account], 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); + assert!(owned_multisig_accounts(owner_3_addr) == vector[], 0); + } + + #[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, OwnedMultisigAccounts { + 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_account(owner_1_addr); + create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 1); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + assert!(owners(multisig_account) == vector[owner_2_addr, owner_3_addr, owner_1_addr], 0); + let multisig_signer = &create_signer(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)] + #[expected_failure(abort_code = 0x30005, location = Self)] + public entry fun test_remove_owners_with_fewer_remaining_than_signature_threshold_should_fail( + owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + 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_account(owner_1_addr); + create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + let multisig_signer = &create_signer(multisig_account); + // Remove 2 owners so there's one left, which is less than the signature threshold of 2. + remove_owners(multisig_signer, vector[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, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + 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_account(owner_1_addr); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2); + + create_transaction(owner_1, multisig_account, PAYLOAD); + 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 == PAYLOAD, 4); + // Automatic yes vote from creator. + assert!(simple_map::length(&transaction.votes) == 1, 5); + assert!(*simple_map::borrow(&transaction.votes, &owner_1_addr), 5); + } + + #[test(owner = @0x123)] + #[expected_failure(abort_code = 0x10004, location = Self)] + public entry fun test_create_transaction_with_empty_payload_should_fail( + owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + create_account(address_of(owner)); + create(owner,1); + let multisig_account = get_next_multisig_account_address(address_of(owner)); + create_transaction(owner, multisig_account, vector[]); + } + + #[test(owner = @0x123, non_owner = @0x124)] + #[expected_failure(abort_code = 0x507D3, location = Self)] + public entry fun test_create_transaction_with_non_owner_should_fail( + owner: &signer, non_owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + create_account(address_of(owner)); + create(owner,1); + let multisig_account = get_next_multisig_account_address(address_of(owner)); + create_transaction(non_owner, multisig_account, PAYLOAD); + } + + #[test(owner = @0x123)] + public entry fun test_create_transaction_with_hashes( + owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + create_account(address_of(owner)); + create(owner,1); + let multisig_account = get_next_multisig_account_address(address_of(owner)); + create_transaction_with_hash(owner, multisig_account, sha3_256(PAYLOAD)); + } + + #[test(owner = @0x123)] + #[expected_failure(abort_code = 0x1000C, location = Self)] + public entry fun test_create_transaction_with_empty_hash_should_fail( + owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + create_account(address_of(owner)); + create(owner,1); + let multisig_account = get_next_multisig_account_address(address_of(owner)); + create_transaction_with_hash(owner, multisig_account, vector[]); + } + + #[test(owner = @0x123, non_owner = @0x124)] + #[expected_failure(abort_code = 0x507D3, location = Self)] + public entry fun test_create_transaction_with_hashes_and_non_owner_should_fail( + owner: &signer, non_owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + create_account(address_of(owner)); + create(owner,1); + let multisig_account = get_next_multisig_account_address(address_of(owner)); + create_transaction_with_hash(non_owner, multisig_account, sha3_256(PAYLOAD)); + } + + #[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, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + 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_account(owner_1_addr); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2); + + create_transaction(owner_1, multisig_account, PAYLOAD); + 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.votes) == 3, 0); + assert!(*simple_map::borrow(&transaction.votes, &owner_1_addr), 1); + assert!(*simple_map::borrow(&transaction.votes, &owner_2_addr), 2); + assert!(*simple_map::borrow(&transaction.votes, &owner_3_addr), 3); + } + + #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)] + public entry fun test_validate_transaction_should_not_consider_removed_owners( + owner_1: &signer, owner_2: &signer, owner_3:& signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + 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_account(owner_1_addr); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2); + + // Owner 1 and 2 approved but then owner 1 got removed. + create_transaction(owner_1, multisig_account, PAYLOAD); + approve_transaction(owner_2, multisig_account, 1); + // Before owner 1 is removed, the transaction technically has sufficient approvals. + assert!(can_be_executed(multisig_account, 1), 0); + let multisig_signer = &create_signer(multisig_account); + remove_owners(multisig_signer, vector[owner_1_addr]); + // Now that owner 1 is removed, their approval should be invalidated and the transaction no longer + // has enough approvals to be executed. + assert!(!can_be_executed(multisig_account, 1), 1); + } + + #[test(owner = @0x123)] + #[expected_failure(abort_code = 0x607D6, location = Self)] + public entry fun test_approve_transaction_with_invalid_transaction_id_should_fail( + owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + create_account(address_of(owner)); + let multisig_account = get_next_multisig_account_address(address_of(owner)); + create(owner, 1); + // Transaction is created with id 1. + create_transaction(owner, multisig_account, PAYLOAD); + approve_transaction(owner, multisig_account, 2); + } + + #[test(owner = @0x123, non_owner = @0x124)] + #[expected_failure(abort_code = 0x507D3, location = Self)] + public entry fun test_approve_transaction_with_non_owner_should_fail( + owner: &signer, non_owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + create_account(address_of(owner)); + let multisig_account = get_next_multisig_account_address(address_of(owner)); + create(owner, 1); + // Transaction is created with id 1. + create_transaction(owner, multisig_account, PAYLOAD); + approve_transaction(non_owner, multisig_account, 1); + } + + #[test(owner = @0x123)] + public entry fun test_approval_transaction_after_rejecting( + owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + let owner_addr = address_of(owner); + create_account(owner_addr); + let multisig_account = get_next_multisig_account_address(owner_addr); + create(owner, 1); + + create_transaction(owner, multisig_account, PAYLOAD); + reject_transaction(owner, multisig_account, 1); + approve_transaction(owner, multisig_account, 1); + let transaction = get_transaction(multisig_account, 1); + assert!(*simple_map::borrow(&transaction.votes, &owner_addr), 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, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + 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_account(owner_1_addr); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2); + + create_transaction(owner_1, multisig_account, PAYLOAD); + 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.votes) == 3, 0); + assert!(!*simple_map::borrow(&transaction.votes, &owner_1_addr), 1); + assert!(!*simple_map::borrow(&transaction.votes, &owner_2_addr), 2); + assert!(!*simple_map::borrow(&transaction.votes, &owner_3_addr), 3); + } + + #[test(owner = @0x123)] + public entry fun test_reject_transaction_after_approving( + owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + let owner_addr = address_of(owner); + create_account(owner_addr); + let multisig_account = get_next_multisig_account_address(owner_addr); + create(owner, 1); + + create_transaction(owner, multisig_account, PAYLOAD); + reject_transaction(owner, multisig_account, 1); + let transaction = get_transaction(multisig_account, 1); + assert!(!*simple_map::borrow(&transaction.votes, &owner_addr), 1); + } + + #[test(owner = @0x123)] + #[expected_failure(abort_code = 0x607D6, location = Self)] + public entry fun test_reject_transaction_with_invalid_transaction_id_should_fail( + owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + create_account(address_of(owner)); + let multisig_account = get_next_multisig_account_address(address_of(owner)); + create(owner, 1); + // Transaction is created with id 1. + create_transaction(owner, multisig_account, PAYLOAD); + reject_transaction(owner, multisig_account, 2); + } + + #[test(owner = @0x123, non_owner = @0x124)] + #[expected_failure(abort_code = 0x507D3, location = Self)] + public entry fun test_reject_transaction_with_non_owner_should_fail( + owner: &signer, non_owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + create_account(address_of(owner)); + let multisig_account = get_next_multisig_account_address(address_of(owner)); + create(owner, 1); + reject_transaction(non_owner, multisig_account, 1); + } + + #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)] + public entry fun test_execute_transaction_successful( + owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + 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_account(owner_1_addr); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2); + + create_transaction(owner_1, multisig_account, PAYLOAD); + // 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); + successful_transaction_execution_cleanup(owner_3_addr, multisig_account,vector[]); + } + + #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)] + public entry fun test_execute_transaction_failed( + owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + 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_account(owner_1_addr); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2); + + create_transaction(owner_1, multisig_account, PAYLOAD); + // 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); + failed_transaction_execution_cleanup(owner_3_addr, multisig_account,vector[], execution_error()); + } + + #[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, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + 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_account(owner_1_addr); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2); + + create_transaction_with_hash(owner_3, multisig_account, sha3_256(PAYLOAD)); + // 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); + successful_transaction_execution_cleanup(owner_3_addr, multisig_account, PAYLOAD); + } + + #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)] + public entry fun test_execute_rejected_transaction( + owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + 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_account(owner_1_addr); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2); + + create_transaction(owner_1, multisig_account, PAYLOAD); + reject_transaction(owner_2, multisig_account, 1); + reject_transaction(owner_3, multisig_account, 1); + assert!(can_be_execute_rejected(multisig_account, 1), 1); + assert!(table::contains(&borrow_global(multisig_account).transactions, 1), 0); + execute_rejected_transaction(owner_3, multisig_account); + } + + #[test(owner = @0x123, non_owner = @0x124)] + #[expected_failure(abort_code = 0x507D3, location = Self)] + public entry fun test_execute_rejected_transaction_with_non_owner_should_fail( + owner: &signer, non_owner: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + create_account(address_of(owner)); + let multisig_account = get_next_multisig_account_address(address_of(owner)); + create(owner,1); + + create_transaction(owner, multisig_account, PAYLOAD); + reject_transaction(owner, multisig_account, 1); + execute_rejected_transaction(non_owner, multisig_account); + } + + #[test(owner_1 = @0x123, owner_2 = @0x124, owner_3 = @0x125)] + #[expected_failure(abort_code = 0x3000A, location = Self)] + public entry fun test_execute_rejected_transaction_without_sufficient_rejections_should_fail( + owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount, OwnedMultisigAccounts { + timestamp::set_time_has_started_for_testing(&create_signer(@0x1)); + 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_account(owner_1_addr); + let multisig_account = get_next_multisig_account_address(owner_1_addr); + create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2); + + create_transaction(owner_1, multisig_account, PAYLOAD); + reject_transaction(owner_2, multisig_account, 1); + execute_rejected_transaction(owner_3, multisig_account); + } +} diff --git a/aptos-move/framework/aptos-framework/sources/voting.move b/aptos-move/framework/aptos-framework/sources/voting.move index 1313bf010fbd1..d03e0497dab20 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 52f331d7fa982..411c30babf73d 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 @@ -271,6 +271,111 @@ 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. + /// + /// Note that this function is not public so it can only be invoked directly instead of via a module or script. This + /// ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to + /// maliciously alter the owners list. + 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. + MultisigAccountCreate { + signatures_required: u64, + }, + + /// 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, + payload: 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, + payload_hash: Vec, + }, + + /// Creates a new multisig account on top of an existing account. + /// + /// This offers a migration path for an existing account with a multi-ed25519 auth key (native multisig account). + /// In order to ensure a malicious module cannot obtain backdoor control over an existing account, a signed message + /// with a valid signature from the account's auth key is required. + MultisigAccountCreateWithExistingAccount { + owners: Vec, + signatures_required: u64, + account_scheme: u8, + account_public_key: Vec, + create_multisig_account_signed_message: 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, + }, + + /// Remove the next transaction if it has sufficient owner rejections. + MultisigAccountExecuteRejectedTransaction { + multisig_account: AccountAddress, + }, + + /// 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. + /// Note that this function is not public so it can only be invoked directly instead of via a module or script. This + /// ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to + /// maliciously alter the owners list. + MultisigAccountRemoveOwners { + owners_to_remove: Vec, + }, + + /// Update the number of signatures required to execute transaction in the specified multisig account. + /// + /// This can only be invoked by the multisig account itself, through the proposal flow. + /// Note that this function is not public so it can only be invoked directly instead of via a module or script. This + /// ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to + /// maliciously alter the number of signatures required. + 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, + }, + /// Entry function that can be used to transfer, if allow_ungated_transfer is set true. ObjectTransferCall { object: AccountAddress, @@ -726,6 +831,57 @@ 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, + } => multisig_account_create(signatures_required), + MultisigAccountCreateTransaction { + multisig_account, + payload, + } => multisig_account_create_transaction(multisig_account, payload), + MultisigAccountCreateTransactionWithHash { + multisig_account, + payload_hash, + } => multisig_account_create_transaction_with_hash(multisig_account, payload_hash), + MultisigAccountCreateWithExistingAccount { + owners, + signatures_required, + account_scheme, + account_public_key, + create_multisig_account_signed_message, + } => multisig_account_create_with_existing_account( + owners, + signatures_required, + account_scheme, + account_public_key, + create_multisig_account_signed_message, + ), + MultisigAccountCreateWithOwners { + additional_owners, + signatures_required, + } => multisig_account_create_with_owners(additional_owners, signatures_required), + MultisigAccountExecuteRejectedTransaction { multisig_account } => { + multisig_account_execute_rejected_transaction(multisig_account) + }, + MultisigAccountRejectTransaction { + multisig_account, + transaction_id, + } => multisig_account_reject_transaction(multisig_account, transaction_id), + MultisigAccountRemoveOwners { owners_to_remove } => { + multisig_account_remove_owners(owners_to_remove) + }, + 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), ObjectTransferCall { object, to } => object_transfer_call(object, to), ResourceAccountCreateResourceAccount { seed, @@ -1568,6 +1724,286 @@ 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. +/// +/// Note that this function is not public so it can only be invoked directly instead of via a module or script. This +/// ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to +/// maliciously alter the owners list. +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. +pub fn multisig_account_create(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!("create").to_owned(), + vec![], + vec![bcs::to_bytes(&signatures_required).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, + payload: 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(&payload).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, + payload_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(&payload_hash).unwrap(), + ], + )) +} + +/// Creates a new multisig account on top of an existing account. +/// +/// This offers a migration path for an existing account with a multi-ed25519 auth key (native multisig account). +/// In order to ensure a malicious module cannot obtain backdoor control over an existing account, a signed message +/// with a valid signature from the account's auth key is required. +pub fn multisig_account_create_with_existing_account( + owners: Vec, + signatures_required: u64, + account_scheme: u8, + account_public_key: Vec, + create_multisig_account_signed_message: 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_existing_account").to_owned(), + vec![], + vec![ + bcs::to_bytes(&owners).unwrap(), + bcs::to_bytes(&signatures_required).unwrap(), + bcs::to_bytes(&account_scheme).unwrap(), + bcs::to_bytes(&account_public_key).unwrap(), + bcs::to_bytes(&create_multisig_account_signed_message).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, +) -> 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(), + ], + )) +} + +/// Remove the next transaction if it has sufficient owner rejections. +pub fn multisig_account_execute_rejected_transaction( + multisig_account: AccountAddress, +) -> 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!("execute_rejected_transaction").to_owned(), + vec![], + vec![bcs::to_bytes(&multisig_account).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. +/// Note that this function is not public so it can only be invoked directly instead of via a module or script. This +/// ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to +/// maliciously alter the owners list. +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()], + )) +} + +/// Update the number of signatures required to execute transaction in the specified multisig account. +/// +/// This can only be invoked by the multisig account itself, through the proposal flow. +/// Note that this function is not public so it can only be invoked directly instead of via a module or script. This +/// ensures that a multisig transaction cannot lead to another module obtaining the multisig signer and using it to +/// maliciously alter the number of signatures required. +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(), + ], + )) +} + /// Entry function that can be used to transfer, if allow_ungated_transfer is set true. pub fn object_transfer_call(object: AccountAddress, to: AccountAddress) -> TransactionPayload { TransactionPayload::EntryFunction(EntryFunction::new( @@ -2969,6 +3405,164 @@ 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()?, + }) + } 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()?, + payload: bcs::from_bytes(script.args().get(1)?).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()?, + payload_hash: bcs::from_bytes(script.args().get(1)?).ok()?, + }, + ) + } else { + None + } + } + + pub fn multisig_account_create_with_existing_account( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some( + EntryFunctionCall::MultisigAccountCreateWithExistingAccount { + owners: bcs::from_bytes(script.args().get(0)?).ok()?, + signatures_required: bcs::from_bytes(script.args().get(1)?).ok()?, + account_scheme: bcs::from_bytes(script.args().get(2)?).ok()?, + account_public_key: bcs::from_bytes(script.args().get(3)?).ok()?, + create_multisig_account_signed_message: bcs::from_bytes(script.args().get(4)?) + .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()?, + }) + } else { + None + } + } + + pub fn multisig_account_execute_rejected_transaction( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some( + EntryFunctionCall::MultisigAccountExecuteRejectedTransaction { + multisig_account: bcs::from_bytes(script.args().get(0)?).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_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 object_transfer_call(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::ObjectTransferCall { @@ -3716,6 +4310,54 @@ static SCRIPT_FUNCTION_DECODER_MAP: once_cell::sync::Lazy Date: Tue, 3 Jan 2023 00:22:00 +0700 Subject: [PATCH 2/9] [API] Add new multisig transaction type --- ...e_when_start_version_is_not_specified.json | 16 +-- ...e_failed_by_entry_function_validation.json | 2 +- ...led_by_invalid_entry_function_address.json | 2 +- ...by_invalid_entry_function_module_name.json | 2 +- ...failed_by_invalid_entry_function_name.json | 2 +- ...d_by_missing_entry_function_arguments.json | 2 +- ...est__test_post_bcs_format_transaction.json | 2 +- api/src/transactions.rs | 37 ++++++ api/types/src/convert.rs | 120 +++++++++++++++--- api/types/src/lib.rs | 2 +- api/types/src/transaction.rs | 37 ++++++ aptos-move/aptos-vm/src/aptos_vm.rs | 24 +++- .../aptos-vm/src/transaction_metadata.rs | 3 + .../executor/src/components/chunk_output.rs | 17 ++- execution/executor/src/mock_vm/mod.rs | 5 + .../generate-format/tests/staged/api.yaml | 31 ++++- types/src/proptest_types.rs | 9 ++ types/src/transaction/mod.rs | 37 +++++- types/src/transaction/multisig.rs | 52 ++++++++ 19 files changed, 357 insertions(+), 45 deletions(-) create mode 100644 types/src/transaction/multisig.rs diff --git a/api/goldens/aptos_api__tests__transactions_test__test_get_transactions_returns_last_page_when_start_version_is_not_specified.json b/api/goldens/aptos_api__tests__transactions_test__test_get_transactions_returns_last_page_when_start_version_is_not_specified.json index b1a036bc44089..6ea16346a5855 100644 --- a/api/goldens/aptos_api__tests__transactions_test__test_get_transactions_returns_last_page_when_start_version_is_not_specified.json +++ b/api/goldens/aptos_api__tests__transactions_test__test_get_transactions_returns_last_page_when_start_version_is_not_specified.json @@ -254,7 +254,7 @@ }, "signature": { "public_key": "0x14418f867a0bd6d42abb2daa50cd68a5a869ce208282481f57504f630510d0d3", - "signature": "0x6cfe245ec2ce64bb2ebed2f6366ac7e50f8e95da94cd5dad458e3969f288d9525ea4c1312c8d3e086abda14357d65142a771b6ac9ec47246f0e2fa024c90180e", + "signature": "0x88170b43190361e40655993f6a169c4e21a284732ffebc25963b3552a924360787e1fa6e22d8ffbd42ac75fcb7258d9258e5c2055f21334ec3c623421af8dd0a", "type": "ed25519_signature" }, "events": [ @@ -532,7 +532,7 @@ }, "signature": { "public_key": "0x14418f867a0bd6d42abb2daa50cd68a5a869ce208282481f57504f630510d0d3", - "signature": "0x8fa482fe30aabf5200d8891a5f037dc91e0f84a44c843ae7cdc7937f6e96d69a44c043412e7008a48a006bebfbeb8a1a35f8ded6b8351b52aa38bb34bbc0b00e", + "signature": "0x2cfd9c73b4ccf74818bad6bc9272cfeda1da83b5f87c5ddbd2819fa3b4b1c5962bd5c6c3960c8a5c4c95bf4fbd3a5105b300d7f7fe8b2376e54bc9cc73773f0a", "type": "ed25519_signature" }, "events": [ @@ -810,7 +810,7 @@ }, "signature": { "public_key": "0x14418f867a0bd6d42abb2daa50cd68a5a869ce208282481f57504f630510d0d3", - "signature": "0x3094b54716c4cf7900ca976dc1582271471591df907366622a39fc62d17b87c02fa88b2f5454615ce3b019310477fbeee5ba808d8465c3f318ffa5786f65dc09", + "signature": "0xe8633d5d4ac2980deaa6893949bec9ef19e2a861b3e259baafc6d496b632366d37a157bec594335a7cb662333a784e6a8e45a45bec9e6623b3656fb3882bf305", "type": "ed25519_signature" }, "events": [ @@ -1088,7 +1088,7 @@ }, "signature": { "public_key": "0x14418f867a0bd6d42abb2daa50cd68a5a869ce208282481f57504f630510d0d3", - "signature": "0xd26e938d26da477f656d9edda518892cbeede12e8ad3da1ccb5fe889be047c2584f578cac231df5184a68f405f3f12cd0db41c77cf6e5d42a9e0a11bca59b500", + "signature": "0x0a83b5ceddafaf0669d2531b6b419e306c1e3c3a076338436abff7e0c173f40e5950ce402c29c5d8578f234ea6d629641b59cac4ef70c7f88873871314e2ca0c", "type": "ed25519_signature" }, "events": [ @@ -1366,7 +1366,7 @@ }, "signature": { "public_key": "0x14418f867a0bd6d42abb2daa50cd68a5a869ce208282481f57504f630510d0d3", - "signature": "0x3da636b94ee39d41c3bd26f60a05cfc7210ba2be448c3d0aa2f23ca5301ebc9f63e190a97bcdf1558824005c738627919a19a24ab0f9a5edb48a1052b697d708", + "signature": "0x2395a77b9fffaeae8c727949a07a10e2f1b36656276baef92e65e0131549aa9f37d410552d0160b6c5f7ceb8e8317980a43cdb87e5e80c4dca0016d1a36b6b0e", "type": "ed25519_signature" }, "events": [ @@ -1644,7 +1644,7 @@ }, "signature": { "public_key": "0x14418f867a0bd6d42abb2daa50cd68a5a869ce208282481f57504f630510d0d3", - "signature": "0x18fb3b0dfcbef70fb66cb6d8fc6f6b73fb5c2f8643892f59dfd07ea47fc7c3d472c3f6b1eaacd0d3935e29532732621bf6873abb4e5ff608622922b032d9a606", + "signature": "0x7f7d42a4aa0b24138d10c79a7bc810d01bf4d9fa3e5ea5c86e3e5d8a3cd6760d7eb876119c1e8ab53fe8529b35618cecd7af979d5e76de18de80f73b89fc3a07", "type": "ed25519_signature" }, "events": [ @@ -1922,7 +1922,7 @@ }, "signature": { "public_key": "0x14418f867a0bd6d42abb2daa50cd68a5a869ce208282481f57504f630510d0d3", - "signature": "0xca705ca68bb833c25d4a13ee1de8ef4bf8895fb2be5cab8846f310880b3f022d87a4b721d64323225ebc817028c167810b1bd5b0f1c61d66104bcc1fff003a0e", + "signature": "0x7a2f88bf8310d1d457ebf7f5fd001ddb2370bf69929caf549657e8178c705154f547e6f1121874597bad87de87a694d0eb927ef5b6b9802e21a3a3c1b2a79e05", "type": "ed25519_signature" }, "events": [ @@ -2200,7 +2200,7 @@ }, "signature": { "public_key": "0x14418f867a0bd6d42abb2daa50cd68a5a869ce208282481f57504f630510d0d3", - "signature": "0x4db4a6430260cce5ea5c708393e71b25805db3e7eff251ecb7f986fcf4c4d66c04abfff78a2edb200ecebc0ea4b0bcc1484f5fef834d838b2f13784189a08004", + "signature": "0xd6306d4db981f1d008cc86f54d01903773647fe4bacf10c3babb6778b2cbd4c5b6a0db6578bcc39f50b2f253da1264a24072d7ed2ba64b7568b7fc4994e59e0b", "type": "ed25519_signature" }, "events": [ diff --git a/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_entry_function_validation.json b/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_entry_function_validation.json index 510b638f5b10e..b39f4130ddde3 100644 --- a/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_entry_function_validation.json +++ b/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_entry_function_validation.json @@ -69,7 +69,7 @@ }, "signature": { "public_key": "0xd5a781494d2bf1a174ddffde1e02cb8881cff6dab70e61cbdef393deac0ce639", - "signature": "0x926363e32e791eaffce0f232429ccd39f2c15e4cb58103a23aa798d374eda9ae7fe21a81ddd0a4c8438584c80ef7b5e033051f6bd86cfc5f2807e40f9156f509", + "signature": "0x2d718527ea968ea0ec77a50deefe03a07d41de634d49a340e5d367a6742f7775323cb925ac8c91720187e5d52da851c4080e8a14f2115ace32bb5b82e426d701", "type": "ed25519_signature" }, "events": [], diff --git a/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_invalid_entry_function_address.json b/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_invalid_entry_function_address.json index fd6b587656543..acab5dd300ef5 100644 --- a/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_invalid_entry_function_address.json +++ b/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_invalid_entry_function_address.json @@ -69,7 +69,7 @@ }, "signature": { "public_key": "0x14418f867a0bd6d42abb2daa50cd68a5a869ce208282481f57504f630510d0d3", - "signature": "0xd6723f1204e18e159e76aa4555bb85c778b5242ef4ba187448e26bc1038404dabb0227616d67aa068c5f0319f5a810b0424719f591756246e4cd1ebea8144906", + "signature": "0x6ec8ff3dc9b311106d89d27332cad39e117b11fc0130d007848498ecd3e28e762f7c3418bba2195d6800d2d8fc393456ae7cb17bfcd2164d1b33d36870ecb205", "type": "ed25519_signature" }, "events": [], diff --git a/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_invalid_entry_function_module_name.json b/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_invalid_entry_function_module_name.json index b89aaa26c517b..1ae539fc3b831 100644 --- a/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_invalid_entry_function_module_name.json +++ b/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_invalid_entry_function_module_name.json @@ -69,7 +69,7 @@ }, "signature": { "public_key": "0x14418f867a0bd6d42abb2daa50cd68a5a869ce208282481f57504f630510d0d3", - "signature": "0x2f19553764e64df8131ec63aaa1638de779287cf4e32e657593b23caa0e8e12744a8d0633329c931aaf5790204d1d2b921ee46d78c61f48f8a5b2bd36c5b6203", + "signature": "0x6201f025db840f8153125d47e1eaac109678ec3f0151785f1b257311e7493230ae025c224b625d908a019424ea11607a22b9d6dc97dd0681a66fe4fc3b6e5d0e", "type": "ed25519_signature" }, "events": [], diff --git a/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_invalid_entry_function_name.json b/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_invalid_entry_function_name.json index c05b28f84bd85..d7dee1ae9d0bb 100644 --- a/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_invalid_entry_function_name.json +++ b/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_invalid_entry_function_name.json @@ -69,7 +69,7 @@ }, "signature": { "public_key": "0x14418f867a0bd6d42abb2daa50cd68a5a869ce208282481f57504f630510d0d3", - "signature": "0x8cb329fa0625c715699f157ec91aff0ecce120ba62b9c16fcabbbc6d8d95fc7ca51c11ccefca3f20e1923eccb7062884007171a2f41220793ebc3c64ab7af40c", + "signature": "0xd18d5a0b13a9736a390887e0776f38390563067bbaf4a9c90aac810ec82e3ca1ffca73b210d3b6af5e8d8bf12c2f6785a6021350269c769f913ba89b0e961d02", "type": "ed25519_signature" }, "events": [], diff --git a/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_missing_entry_function_arguments.json b/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_missing_entry_function_arguments.json index c48eb24d5b853..82ab2ac68ccfb 100644 --- a/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_missing_entry_function_arguments.json +++ b/api/goldens/aptos_api__tests__transactions_test__test_get_txn_execute_failed_by_missing_entry_function_arguments.json @@ -68,7 +68,7 @@ }, "signature": { "public_key": "0x14418f867a0bd6d42abb2daa50cd68a5a869ce208282481f57504f630510d0d3", - "signature": "0x94dc61088c975f1b0deb79b49e535fc759c70aa258efe673e100738e6c077b54ad5c3f9bae85cb13d0e0085185dba47a65ff8b345b8733c3cc1c6e2a47f34e0a", + "signature": "0xbd7abbbbbdab23ebbb1fbdb853e5200bbd7d6d31a52630e761b93fc9ee0d18cab421930d95c67479c9d63e7f780a452c6af4ae6871ddbeafd4db5eb6399a0902", "type": "ed25519_signature" }, "events": [], diff --git a/api/goldens/aptos_api__tests__transactions_test__test_post_bcs_format_transaction.json b/api/goldens/aptos_api__tests__transactions_test__test_post_bcs_format_transaction.json index 693b6df1614e6..b754e4702d6f1 100644 --- a/api/goldens/aptos_api__tests__transactions_test__test_post_bcs_format_transaction.json +++ b/api/goldens/aptos_api__tests__transactions_test__test_post_bcs_format_transaction.json @@ -15,7 +15,7 @@ }, "signature": { "public_key": "0x14418f867a0bd6d42abb2daa50cd68a5a869ce208282481f57504f630510d0d3", - "signature": "0x066c59a294193ab4fcde6fc80aab99b95422881f2f82cbcab5d67e06816fbd1d70890c4f4098aff38cf3317a249313a73b9db410ccaa73915beb4e99617fd208", + "signature": "0xb1b959c537f815029fb0b2056aaa3592925e8e97d1d37bfcebce5d11de798289574d427be780e84a26d8293a9128090a77ea4aefe774e7b0c7adb229973d2f0e", "type": "ed25519_signature" } } diff --git a/api/src/transactions.rs b/api/src/transactions.rs index b9a0b8b6bb473..d3aaf4f3377ec 100644 --- a/api/src/transactions.rs +++ b/api/src/transactions.rs @@ -897,6 +897,43 @@ impl TransactionsApi { })?; } }, + TransactionPayload::Multisig(multisig) => { + if let Some(payload) = &multisig.transaction_payload { + verify_module_identifier(payload.module().name().as_str()) + .context("Transaction entry function module invalid") + .map_err(|err| { + SubmitTransactionError::bad_request_with_code( + err, + AptosErrorCode::InvalidInput, + ledger_info, + ) + })?; + + verify_function_identifier(payload.function().as_str()) + .context("Transaction entry function name invalid") + .map_err(|err| { + SubmitTransactionError::bad_request_with_code( + err, + AptosErrorCode::InvalidInput, + ledger_info, + ) + })?; + for arg in payload.ty_args() { + let arg: MoveType = arg.into(); + arg.verify(0) + .context("Transaction entry function type arg invalid") + .map_err(|err| { + SubmitTransactionError::bad_request_with_code( + err, + AptosErrorCode::InvalidInput, + ledger_info, + ) + })?; + } + } + }, + + // Deprecated. Will be removed in the future. TransactionPayload::ModuleBundle(_) => {}, } // TODO: Verify script args? diff --git a/api/types/src/convert.rs b/api/types/src/convert.rs index 73f366b6ab707..515ffbe68158b 100644 --- a/api/types/src/convert.rs +++ b/api/types/src/convert.rs @@ -5,8 +5,9 @@ use crate::{ transaction::{ DecodedTableData, DeleteModule, DeleteResource, DeleteTableItem, DeletedTableData, - ModuleBundlePayload, StateCheckpointTransaction, UserTransactionRequestInner, WriteModule, - WriteResource, WriteTableItem, + ModuleBundlePayload, MultisigPayload, MultisigTransactionPayload, + StateCheckpointTransaction, UserTransactionRequestInner, WriteModule, WriteResource, + WriteTableItem, }, view::ViewRequest, Bytecode, DirectWriteSet, EntryFunctionId, EntryFunctionPayload, Event, HexEncodedBytes, @@ -27,7 +28,8 @@ use aptos_types::{ table::TableHandle, }, transaction::{ - EntryFunction, ExecutionStatus, ModuleBundle, RawTransaction, Script, SignedTransaction, + EntryFunction, ExecutionStatus, ModuleBundle, Multisig, RawTransaction, Script, + SignedTransaction, }, vm_status::AbortLocation, write_set::WriteOp, @@ -164,12 +166,6 @@ impl<'a, R: MoveResolverExt + ?Sized> MoveConverter<'a, R> { use aptos_types::transaction::TransactionPayload::*; let ret = match payload { Script(s) => TransactionPayload::ScriptPayload(s.try_into()?), - ModuleBundle(modules) => TransactionPayload::ModuleBundlePayload(ModuleBundlePayload { - modules: modules - .into_iter() - .map(|module| MoveModuleBytecode::from(module).try_parse_abi()) - .collect::>>()?, - }), EntryFunction(fun) => { let (module, function, ty_args, args) = fun.into_inner(); let func_args = self @@ -195,6 +191,47 @@ impl<'a, R: MoveResolverExt + ?Sized> MoveConverter<'a, R> { type_arguments: ty_args.into_iter().map(|arg| arg.into()).collect(), }) }, + Multisig(multisig) => { + let transaction_payload = if let Some(payload) = multisig.transaction_payload { + let (module, function, ty_args, args) = payload.into_inner(); + let func_args = self + .inner + .view_function_arguments(&module, &function, &args); + let json_args = match func_args { + Ok(values) => values + .into_iter() + .map(|v| MoveValue::try_from(v)?.json()) + .collect::>()?, + Err(_e) => args + .into_iter() + .map(|arg| HexEncodedBytes::from(arg).json()) + .collect::>()?, + }; + + Some(MultisigTransactionPayload { + arguments: json_args, + function: EntryFunctionId { + module: module.into(), + name: function.into(), + }, + type_arguments: ty_args.into_iter().map(|arg| arg.into()).collect(), + }) + } else { + None + }; + TransactionPayload::MultisigPayload(MultisigPayload { + multisig_address: multisig.multisig_address.into(), + transaction_payload, + }) + }, + + // Deprecated. Will be removed in the future. + ModuleBundle(modules) => TransactionPayload::ModuleBundlePayload(ModuleBundlePayload { + modules: modules + .into_iter() + .map(|module| MoveModuleBytecode::from(module).try_parse_abi()) + .collect::>>()?, + }), }; Ok(ret) } @@ -533,15 +570,6 @@ impl<'a, R: MoveResolverExt + ?Sized> MoveConverter<'a, R> { args, )) }, - TransactionPayload::ModuleBundlePayload(payload) => { - Target::ModuleBundle(ModuleBundle::new( - payload - .modules - .into_iter() - .map(|m| m.bytecode.into()) - .collect(), - )) - }, TransactionPayload::ScriptPayload(script) => { let ScriptPayload { code, @@ -567,6 +595,62 @@ impl<'a, R: MoveResolverExt + ?Sized> MoveConverter<'a, R> { None => return Err(anyhow::anyhow!("invalid transaction script bytecode")), } }, + TransactionPayload::MultisigPayload(multisig) => { + let transaction_payload = if let Some(payload) = multisig.transaction_payload { + let MultisigTransactionPayload { + function, + type_arguments, + arguments, + } = payload; + + let module = function.module.clone(); + let code = self.inner.get_module(&module.clone().into())? as Rc; + let func = code + .find_entry_function(function.name.0.as_ident_str()) + .ok_or_else(|| { + format_err!("could not find entry function by {}", function) + })?; + ensure!( + func.generic_type_params.len() == type_arguments.len(), + "expect {} type arguments for entry function {}, but got {}", + func.generic_type_params.len(), + function, + type_arguments.len() + ); + + let args = self + .try_into_vm_values(func, arguments)? + .iter() + .map(bcs::to_bytes) + .collect::>()?; + Some(aptos_types::transaction::MultisigTransactionPayload { + module: module.into(), + function: function.name.into(), + ty_args: type_arguments + .into_iter() + .map(|v| v.try_into()) + .collect::>()?, + args, + }) + } else { + None + }; + Target::Multisig(Multisig { + multisig_address: multisig.multisig_address.into(), + transaction_payload, + }) + }, + + // Deprecated. Will be removed in the future. + TransactionPayload::ModuleBundlePayload(payload) => { + Target::ModuleBundle(ModuleBundle::new( + payload + .modules + .into_iter() + .map(|m| m.bytecode.into()) + .collect(), + )) + }, }; Ok(ret) } diff --git a/api/types/src/lib.rs b/api/types/src/lib.rs index 9d6d5978d0fe9..ad09a8bd7ac64 100644 --- a/api/types/src/lib.rs +++ b/api/types/src/lib.rs @@ -16,7 +16,7 @@ mod ledger_info; pub mod mime_types; mod move_types; mod table; -mod transaction; +pub mod transaction; mod view; mod wrappers; diff --git a/api/types/src/transaction.rs b/api/types/src/transaction.rs index 5a9abfb29ebb5..7e0dcaeb47c7b 100755 --- a/api/types/src/transaction.rs +++ b/api/types/src/transaction.rs @@ -583,7 +583,9 @@ pub enum GenesisPayload { pub enum TransactionPayload { EntryFunctionPayload(EntryFunctionPayload), ScriptPayload(ScriptPayload), + // Delegated. Will be removed in the future. ModuleBundlePayload(ModuleBundlePayload), + MultisigPayload(MultisigPayload), } impl VerifyInput for TransactionPayload { @@ -591,6 +593,8 @@ impl VerifyInput for TransactionPayload { match self { TransactionPayload::EntryFunctionPayload(inner) => inner.verify(), TransactionPayload::ScriptPayload(inner) => inner.verify(), + TransactionPayload::MultisigPayload(inner) => inner.verify(), + // Delegated. Will be removed in the future. TransactionPayload::ModuleBundlePayload(inner) => inner.verify(), } } @@ -666,6 +670,39 @@ impl TryFrom