Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multisig deployment/management solution #33

Closed
alnoki opened this issue Nov 29, 2022 · 12 comments
Closed

Add multisig deployment/management solution #33

alnoki opened this issue Nov 29, 2022 · 12 comments

Comments

@alnoki
Copy link
Member

alnoki commented Nov 29, 2022

Architecture

Presently, Econia provides several public entry points that recognize the authority of a single signer, @econia, the address under which the upgradeable Move package is published.

These entry public entry points include several major functions:

Recognized market management

Given that Econia supports permissionless market registration, it also supports a "recognized market list" that maps from a trading pair (e.g. APT/USDC) to metadata about a corresponding recognized market. More specifically, since anyone can register an APT/USDC market with an arbitrary lot size/tick size combination, the recognized market list is designed to refer users to a recognized market with optimal trading parameters. This approach prevents the fracturing of liquidity across multiple markets that support the same asset pairs.

All recognized market management functions are public entry functions that require the signature of @econia.

Incentive parameter management

Econia maintains a set of on-chain "incentive parameters" that describe the flat fee charged to takers, the cost to register a market, etc. These parameters are set to hard-coded genesis values upon module publication and can be updated later via a call to incentives::update_incentives(), a public entry function that requires the signature of @econia.

Fee account operations

Econia collects a portion of taker fees for each market, denominated in the quote currency for the market (e.g. USDC for wBTC/USDC), as well as "utility coins" that are charged for operations like registering a market, registering as a custodian, etc. Initially, utility coins fees are denominated in APT. Public fee account functions require the signature of @econia, and only some are public entry functions.

Proposed multisig support

Multisig support is proposed for the following functions:

Module publication

A multisig @econia account shall support module publication, in the following format:

  • A signatory provides a public record of the package to be published, (e.g. on GitHub).
  • Other signatories locally compute a checksum/hash on the package to be published.
  • The signatory submits a transaction to the multisig account proposing module publication.
  • Other signatories verify the on-chain hash of the publication proposal against the public hash.
  • Other signatories sign off on the module publication, on-chain.
  • The module is published under @econia.

A similar workflow shall apply for module upgrades.

Function and script invocation

A multisig @econia account shall support a similar workflow for public entry point invocations, whether via public direct public entry function invocation or via scripts. In the case of a script, a similar workflow shall apply:

  • A signatory provides a public record of the Move script to be executed, (e.g. on GitHub).
  • Other signatories locally compute a checksum/hash on the script to be executed.
  • The signatory submits a transaction to the multisig account proposing script execution.
  • Other signatories verify the on-chain hash of the script proposal against the public hash.
  • Other signatories sign off on the script execution proposal, on-chain.
  • The script is executed under the authority of @econia.

In the case of direct public entry function invocation, a similar hash verification paradigm shall apply, whereby signatories can verify a proposed invocation payload. However, given that the most practical public entry functions accepts vectors as arguments, it may be most straightforward for all invocations to assume a script format.

Signatory designation

The multisig account shall support modifications to the signatory manifest, e.g. adding and removing signatories from the account per a vote by other signatories on the account. Signatories shall also be able to voluntarily leave at any time, but steps shall be taken to ensure that threshold requirements are met following a reduction in the number of signatories. The multisig account shall support quorum functionality and modification of the relevant parameters.

Chronological sensitivity

The multisig account shall optionally support time-sensitive restrictions, e.g. that a publication or script will not execute after a certain time has passed. Similarly, it shall prevent old transactions from re-executing, potentially by way of sequence numbers.

@JackyWYX

@JackyWYX
Copy link

Some specs from Momentum Safe

  1. For contract deployment, we already have a "verifyHash" which is the hash of contract metadata and contract byte code. The hash can be checked for all stakeholders.
  2. Currently, only entry function call is supported in current implementation of SDK. Please send us the script for testing so that we may have the script tested with Momentum Safe.
  3. There is a Aptos built-in expiration time that can be set during the initialization of a transaction which can be taken a a chronological restriction. If a transaction is expired, you will need to "reject a transaction", aka sending an empty transaction with the same nonce to overwrite the expired one.

Please let us know if there is anything that might help on our end :)

@alnoki
Copy link
Member Author

alnoki commented Nov 29, 2022

  1. Thanks for noting this feature, as it will be helpful for module deployment and upgrades.
  2. For initial maintenance operations public entry function calls will be sufficient, so long as vector arguments are supported. For example as in registry::set_recognized_markets() and incentives::update_incentives(). If the multisig were to set recognized markets and update incentives concurrently, however, a simple script that executes both public entry function calls would be more convenient than having all signatories approve two separate transactions. I'll note that such a script would have to support passing an @econia signature (the signature of the multisig wallet). Something like:
script {
    // Uses.
    use econia::{incentives, registry};

    // Incentive parameters to set.
    const MARKET_REGISTRATION_FEE: u64 = 1;
    const UNDERWRITER_REGISTRATION_FEE: u64 = 2;
    const CUSTODIAN_REGISTRATION_FEE: u64 = 3;
    const TAKER_FEE_DIVISOR: u64 = 2000;
    const FEE_SHARE_DIVISOR_0: u64 = 10000;
    const FEE_SHARE_DIVISOR_1: u64 = 5000;
    const TIER_ACTIVATION_FEE_0: u64 = 0;
    const TIER_ACTIVATION_FEE_1: u64 = 1;
    const WITHDRAWAL_FEE_0: u64 = 2;
    const WITHDRAWAL_FEE_1: u64 = 1;

    // Recognized market IDs to set.
    const MARKET_ID_APT_USDC: u64 = 123;
    const MARKET_ID_WBTC_USDC: u64 = 456;

    // Main function.
    fun invoke_two_public_entry_functions(econia: &signer) {
        // Update incentives.
        incentives::update_incentives(
            econia,
            MARKET_REGISTRATION_FEE,
            UNDERWRITER_REGISTRATION_FEE,
            CUSTODIAN_REGISTRATION_FEE,
            TAKER_FEE_DIVISOR,
            vector[vector[FEE_SHARE_DIVISOR_0,
                          TIER_ACTIVATION_FEE_0,
                          WITHDRAWAL_FEE_0],
                   vector[FEE_SHARE_DIVISOR_1,
                          TIER_ACTIVATION_FEE_1,
                          WITHDRAWAL_FEE_1]]
        );
        // Set recognized markets. 
        registry::set_recognized_markets(
            econia,
            vector[MARKET_ID_APT_USDC, MARKET_ID_WBTC_USDC];
        )
    }
}
  1. Thanks for noting this feature. A transaction expiry of a day or two will probably end up providing ample time for all signatories to sign, after sufficient off-chain discussion/approval. As for "rejecting a transaction", does this require that all signatories in the wallet approve the rejection? Separately, can expiries be applied to script invocations as well?

@alnoki
Copy link
Member Author

alnoki commented Dec 13, 2022

Signatory mutation

Presently, Aptos' multi-ed25519 signer schema does not support modifications to the signatory list: if one signer is compromised, they cannot be removed from the multisig wallet. Similarly, a new signer cannot be added later.

Instead, a resource account bootstrapping methodology is proposed, whereby an immutable bootstrapper package is published by a single signer, who initializes a governance resource account therein. The governance account has a compatible publication policy, hence once the governance account has been bootstrapped, it can self-upgrade.

The resource account then manages operations via a governance capability defined in the bootstrapper package, which allows the resource account to obtain a signer via a voting-based proposal paradigm:

Package snippets

Bootstrapper

[package]
name = 'Econia Governance Bootstrapper'
version = '1.0.0' # Per SemVer
upgrade_policy = 'immutable'

[addresses]
# Single signer.
governance_bootstrapper = '0x123abc...'
module econia_governance_bootstrapper::bootstrapper {

    use aptos_framework::account::{Self, SignerCapability};
    use aptos_framework::code;
    use aptos_framework::timestamp;
    use std::bcs;
    use std::signer::address_of;

    /// Authentication token for getting governance account signer.
    struct GovernanceCapability has store {}

    /// Bootstrapper resource.
    struct Bootstrapper has key {
        /// Signer capability for governance account.
        signer_capability: SignerCapability,
        /// `false` before governance module bootstrapped at governance
        /// account, `true` after.
        bootstrapped: bool
    }

    /// Initialize governance bootstrapper module, using time seed as in 
    /// econia::resource_account::init_module().
    fun init_module(
        governance_bootstrapper: &signer
    ) {
        /// Get time seed for governance resource account.
        let time_seed = bcs::to_bytes(&timestamp::now_microseconds());
        // Create resource account, storing signer capability.
        let (_, signer_capability) = account::create_resource_account(
            governance_bootstrapper, time_seed);
        // Flag governance package not bootstrapped.
        let bootstrapped = false;
        // Store boostrapper resource under bootstrapper account.
        move_to(governance_bootstrapper,
                Bootstrapper{signer_capability, bootstrapped});
    }

    /// Called by governance bootstrapper module during governance
    /// genesis.
    public entry fun bootstrap_governance_package(
        account: &signer,
        metadata_serialized: vector<u8>,
        code: vector<vector<u8>>
    ) acquires Bootstrapper {
        // Assert caller is governance bootstrapper.
        assert!(address_of(account) == @governance_bootstrapper, 0);
        // Immutably borrow bootstrapper resource. 
        let bootstrapper_ref =
            &borrow_global<Bootstrapper>(@governance_bootstrapper);
        // Assert governance not already bootstrapped.
        assert!(bootstrapper_ref.bootstrapped == false, 0);
        // Get governance account signer.
        let governance_signer = account::create_signer_with_capability(
            &bootstrapper_ref.signer_capability)
        // Publish code under governance account.
        code::publish_package_txn(
            governance_signer, metadata_serialized, code);
    }

    /// Called by governance account during bootstrapping.
    public fun get_governance_capability(
        account: &signer,
    ): GovernanceCapability
    acquires Bootstrapper {
        // Mutably borrow bootstrapper resource.
        let bootstrapper_ref_mut = 
            &mut borrow_global_mut<Bootstrapper>(@governance_bootstrapper);
        // Get governance account address.
        let governance_account_address =
            account::get_signer_capability_address(
                &governance_info_ref_mut.signer_capability)
        // Assert calling account is governance account.
        assert!(address_of(account) == governance_account_address, 0);
        // Assert governance not already bootstrapped.
        assert!(bootstrapper_ref_mut.bootstrapped == false, 0);
        // Flag that governance has been bootstrapped.
        bootstrapper_ref_mut.bootstrapped = true;
        GovernanceCapability{} // Return governance capability.
    }

    /// Return governance signer, provided governance capability.
    public fun get_governance_signer(
        governance_capability_ref: &GovernanceCapability
    ): signer
    acquires Bootstrapper {
        // Immutably borrow bootstrapper resource. 
        let bootstrapper_ref =
            &borrow_global<Bootstrapper>(@governance_bootstrapper);
        // Return governance account signer.
        account::create_signer_with_capability(
            &bootstrapper_ref.signer_capability)
    }
}

Governance

[package]
name = 'Econia Governance'
version = '1.0.0' # Per SemVer
upgrade_policy = 'compatible'

[addresses]
# Generated during bootstrapper initialization.
governance_account = '0x321def...'
module econia_governance::governance {

    use aptos_framework::simple_map::{Self, SimpleMap};
    use aptos_framework::table_with_length::{Self, TableWithLength};
    use aptos_framework::voting;
    use econia::tablist{Self, Tablist};
    use econia_governance_bootstrapper::bootstrapper{
        Self, GovernanceCapability};

    /// Gensis signatories.
    const GENESIS_SIGNATORY_0: address = 0xabc123;
    const GENESIS_SIGNATORY_1: address = 0xdef321;
    const GENESIS_SIGNATORY_2: address = 0x321abc;
    const GENESIS_SIGNATORY_3: address = 0x456cba;
    const GENESIS_SIGNATORY_4: address = 0x789fed;

    /// Resource to store governance capability.
    struct GovernanceCapabilityStore has key {
        governance_capability: GovernanceCapability
    }

    /// Proposal to publish code.
    struct PublicationProposal has store {}

    /// Proposal to invoke Econia via governance account.
    struct InvocationProposal has store {}

    /// Tracks signatories to the governance account.
    struct Signatories has key {
        addresses: vector<address>
    }

    /// Record of votes for a given proposal.
    struct ProposalVotes has store {
        /// Map from signatory to vote. None if no vote, else their vote
        /// on the proposal.
        votes: SimpleMap<address, option<bool>>
    }

    /// Record of all votes for a given proposal type.
    struct Votes<phantom ProposalType> has key {
        /// Map from proposal ID to proposal votes.
        proposals: TableWithLength<u64, ProposalVotes>
    }

    /// Initialize governance module.
    fun init_module(
        account: &signer
    ) {
        let governance_capability = bootstrapper::get_governance_capability(
            account); // Get governance capability.
        // Store governance capability in a resource.
        let governance_capability_store =
            GovernanceCapabilityStore{governance_capability};
        // Move resource to governance account.
        move_to<GovernanceCapabilityStore>(
            account, GovernnanceCapabilityStore);
        // Register publication proposal voting forum.
        voting::register<PublicationProposal>(account);
        // Register invocation proposal voting forum.
        voting::register<InvocationProposal>(account);
        move_to<Signatories>(account, Signatories{addresses: vector[
            GENESIS_SIGNATORY_0,
            GENESIS_SIGNATORY_1,
            GENESIS_SIGNATORY_2,
            GENESIS_SIGNATORY_3,
            GENESIS_SIGNATORY_4]}); // Initialize genesis signatories.
        /// Move publication proposal votes tracker to account.
        move_to<Votes<PublicationProposal>>(account, Votes{
            proposals: table_with_length::new()});
        /// Move invocation proposal votes tracker to account.
        move_to<Votes<InvocationProposal>>(account, Votes{
            proposals: table_with_length::new()});
    }

    /// Return governance account signer.
    fun get_governance_signer():
    signer {
        // Immutably borrow governance capability.
        let goverance_capability_ref =
            &borrow_global<GovernanceCapabilityStore>(@governance_account).
                governance_capability;
        // Return governance signer.
        bootstrapper::get_governance_signer(governance_capability_ref)
    }

    /// Return signer for a governance publication.
    public fun get_publication_signer(
        proposal_capability: PublicationProposal
    ): signer {
        // Unpack proposal capability.
        let PublicationProposal{} = proposal_capability;
        get_governance_signer() // Return governance signer.
    }

    /// Return signer for a governance invocation.
    public fun get_invocation_signer(
        proposal_capability: InvocationProposal
    ): signer {
        // Unpack proposal capability.
        let InvocationProposal{} = proposal_capability;
        get_governance_signer() // Return governance signer.
    }

}

Initialization

Here, bootstrapper::init_module() creates the governance resource account, and the governance package is published to it via bootstrapper::bootstrap_governance_package(). During governance::init_module() a voting forum is created via aptos_framework::voting::register(), and several data structures are created to track the signatories to the account.

Operations

Operations from the governance account will thus obtain a signature via bootstrapper::get_governance_signer(), passing the GovernanceCapability stored during bootstrapping. This will enable publication of the Econia package via aptos_framework::code module (including upgrades), and invocations of functions requiring the @econia signature (the governance account) like econia::incentives::update_incentives().

Wrappers will be required to ensure that signatories only vote once per proposal, that proposal capabilities are creating properly during proposals, that only signatories can create proposals, etc.

Script resolution

When a governance script is executed, it shall call aptos_framework::voting::resolve(), receiving a corresponding proposal capability in return. The capability can then be passed to either get_publication_signer() or get_invocation_signer().

@JackyWYX @BriungRi

@JackyWYX
Copy link

  1. We are aware of the issue, and we have discussed with Aptos team for quite a while. From what we heard from Aptos, key rotation for multi-sig shall be ready on Aptos next release along with domain specific signatures. Momentum Safe will implement the feature accordingly after the change.
  1. The governance script is definitely going to work. The only issue we see in this solution is that:
  • It does not provide the user friendly interface for governors, which in returns will potentially put some limit to the diversity of governors.
  • From Aptos default key store, the private keys are stored as a plain text in a yaml file (correct me if I am wrong). Some users may need to copy their wallet private key as the plain text, which opens some vulnerability to the security of private keys. In addition, Aptos CLI does not support hardware wallets.

These are some trade-offs I see in the two options. Would love to have more ideas about this topic.

@alnoki
Copy link
Member Author

alnoki commented Dec 13, 2022

@JackyWYX thank you for all of the input here. At Move Monday yesterday, there was some talk about a multi-account strategy as well, but depending on the timing of these releases a more custom approach might be necessary.

The aptos_framework::voting::resolve() script execution is instrumental for Econia governance operations, and agreed that a friendly interface will be key: ideally we can abstract it to a simple public entry fun with vote yes or no on a proposal ID (just needs a signer, a bool, and a u64), such that signatories can at least sign off on votes with ease.

Proposing will of course be a bit trickier, and it would be helpful to have a UX that allows proposers to simply draft scripts then have the hash etc. get converted into a vote proposal in the background.

An approach involving plain text keys in a yaml file is unquestionably a security risk. If we can get the right wrappers (like the public entry fun per above), then signatories can do independent key management.

@alnoki
Copy link
Member Author

alnoki commented Dec 13, 2022

Modifications

SignerCapability deployer approach

Rather than passing back and forth a GovernanceCapability between the governance account and a bootstrapper account, it would be more straightforward to store the SignerCapability in the governance account itself. This approach produces the following deployer package:

[package]
name = 'Econia Governance Deployer'
version = '1.0.0' # Per SemVer
upgrade_policy = 'immutable'

[addresses]
# Conventional signer.
governance_deployer = '0x123abc...'
module econia_governance_deployer::deployer {

    /// Stores governance account signer capability during genesis.
    struct SignerCapabilityStore has key {
        /// Some upon module publication, none after goverance package
        /// publication.
        signer_capability: option<SignerCapability>
    }

    /// Initialize governance deployer module, using time seed as in
    /// econia::resource_account::init_module().
    fun init_module(
        account: &signer
    ) {
        // Get time seed for governance resource account.
        let time_seed = bcs::to_bytes(&timestamp::now_microseconds());
        // Create resource account, getting signer capability.
        let (_, signer_capability) =
            account::create_resource_account(account, time_seed);
        // Store signer capability.
        move_to(account, SignerCapabilityStore{
            signer_capability: option::some(signer_capability)});
    }

    /// Called by governance deployer account during genesis.
    public entry fun deploy_governance_package(
        account: &signer,
        metadata_serialized: vector<u8>,
        code: vector<vector<u8>>
    ) acquires Bootstrapper {
        assert!(address_of(account) == @governance_deployer, 0);
        // Immutably borrow signer capability store.
        let signer_capability_store_ref = &borrow_global<
            SignerCapabilityStore>(@governance_deployer);
        // Immutably borrow signer capability.
        let signer_capability_ref =
            option::borrow_some(signer_capability_store_ref)
        // Get governance account signer.
        let governance_signer = account::create_signer_with_capability(
            signer_capability_ref);
        // Publish code under governance account.
        code::publish_package_txn(
            governance_signer, metadata_serialized, code);
    }

    /// Return governance signer capability during genesis.
    public fun get_signer_capability(
        account: &signer,
    ): SignerCapability
    acquires SignerCapabilityStore {
        // Mutably borrow signer capability store.
        let signer_capability_store_ref_mut = &mut borrow_global_mut<
            SignerCapabilityStore>(@governance_deployer);
        // Get signer capability from option.
        let signer_capability = option::destroy_some(
            signer_capability_store_ref_mut.signer_capability);
        // Get governance account address.
        let governance_account_address =
            account::get_signer_capability_address(&signer_capability);
        // Assert calling account is governance account.
        assert!(address_of(account) == governance_account_address, 0);
        signer_capability // Return signer capability.
    }
}

Here, genesis involves the following steps:

  1. econia_governance_deployer::deployer::init_module() creates the governance resource account, storing its signer capability in an option.
  2. A public entry call to econia_governance_deployer::deployer::deploy_governance_package() publishes the governance package, which contains an init_module() call that invokes econia_governance_deployer::deployer::get_signer_capability().
  3. The governance accounts stores the SignerCapability itself.

Econia goverance package data structures

  1. Per above, the governance package would replace the GovernanceCapabilityStore with a SignerCapabilityStore and generate a signer from it as needed.
  2. PublicationProposal and InvocationProposal can be simplified to a single GovernanceProposal.
[package]
name = 'Econia Governance'
version = '1.0.0' # Per SemVer
upgrade_policy = 'compatible'

[addresses]
# Generated during bootstrapper initialization.
governance_account = '0x321def...'
# Same as in deployer package.
governance_deployer = '0x123abc...'

[dependencies.econia_governance_deployer]
git = ...
subdir = ...
rev = ...
module econia_governance::governance {

    /// Gensis signatories.
    const GENESIS_SIGNATORY_0: address = 0xabc123;
    const GENESIS_SIGNATORY_1: address = 0xdef321;
    const GENESIS_SIGNATORY_2: address = 0x321abc;
    const GENESIS_SIGNATORY_3: address = 0x456cba;
    const GENESIS_SIGNATORY_4: address = 0x789fed;

    /// Resource to store signer capability.
    struct SignerCapabilityStore has key {
        signer_capability: SignerCapability
    }

    /// Generic governance proposal capability.
    struct GovernanceProposal has store {}

    /// Tracks signatories to the governance account.
    struct Signatories has key {
        addresses: vector<address>
    }

    /// Record of all votes.
    struct Votes has key {
        /// Map from proposal ID to map from address to vote on given
        /// proposal ID.
        proposals: TableWithLength<u64, SimpleMap<address, option<bool>>
    }

    /// Initialize governance module.
    fun init_module(
        account: &signer
    ) {
        // Get signer capability.
        let signer_capability = deployer::get_signer_capability(account);
        // Store signer capability under governance account
        // Move resource to governance account.
        move_to<SignerCapabilityStore>(
            account, SignerCapabilityStore{signer_capability});
        // Register governance proposal voting forum.
        voting::register<GovernanceProposal>(account);
        move_to<Signatories>(account, Signatories{addresses: vector[
            GENESIS_SIGNATORY_0,
            GENESIS_SIGNATORY_1,
            GENESIS_SIGNATORY_2,
            GENESIS_SIGNATORY_3,
            GENESIS_SIGNATORY_4]}); // Initialize genesis signatories.
        /// Move publication proposal votes tracker to account.
        move_to<Votes>(account, Votes{proposals: table_with_length::new()});
    }

    /// Return signer for a governance proposal resolution.
    ///
    /// Called during a proposal resolution script after the
    /// `GovernanceProposal` capability has been returned by
    /// `aptos_framework::voting::resolve()`.
    public fun get_proposal_resolution_signer(
        proposal_capability: GovernanceProposal
    ): signer {
        // Unpack proposal capability.
        let GovernanceProposal{} = proposal_capability;
        // Borrow signer capability.
        let signer_capability_ref = &borrow_global<SignerCapabilityStore>(
            @governance_account).signer_capability;
        // Return governance account signer.
        account::create_signer_with_capability(signer_capability_ref)
    }

}

Considerations

Presently, Econia contains a simulation function, econia::market::index_orders_sdk(), which relies on a public key for initiation.
With a resource account approach, it will be necessary to either:

  1. Determine the public key for the resource account, or
  2. Run a simulation without a public key.

@alnoki
Copy link
Member Author

alnoki commented Dec 15, 2022

Multi-ED25519 approach

Given the technical complexity associated with the dual deployer/governance package schema above, a proposed alternative is to use a standard multi-ED25519 account scheme with authentication key rotation. Here, if signatories need to be added or removed, or a quorum needs to be updated, the operation can be done through multi-ED25519 functionality. This may involve aptos_framework::account::rotate_authentication_key() in the case that signatories need to be added or removed.

This approach could potentially involve off-chain signature assemblage, though proofs could still be posted on-chain. Ideally the multi-ED25519 account would support script execution as well as package upgrades, in a form that allows for hash-based script verification per above.

A potential security concern with this approach is a botched authentication key rotation, e.g. accidentally locking everyone out of the multi-ED25519 account.

Notably, if a fully on-chain strategy (e.g. a dual deployer/governance package) were to be adopted in the future, the multi-ED25519 account could simply have its authentication key rotated to have as a single signer the fully on-chain governance resource account. This approach assumes, however, that the public key for a resource account can be determined. Here, note that aptos_framework::resource_acccount includes assorted wrapper APIs for resource account creation that may help, e.g. create_resource_account_and_fund(), which accepts an optional authentication key and a funding amount.

Hence, required support for a multi-ED25519 approach:

  1. Script invocation
  2. Signatory group modification, potentially via authentication key rotation
  3. Quorum modification (e.g. if 4 signatories join, increase quorum by 2)
  4. Module publication
  5. Potential future rotation to a governance resource account

@chen-robert @JackyWYX

@alnoki
Copy link
Member Author

alnoki commented Jan 26, 2023

@JackyWYX For package deployment and maintenance, the following functionalities shall be supported:

Auth key rotation

Upon deployment, it is proposed that a multisig account first be established under a vanity account address.
Here, a hot wallet can be generated corresponding to 0xc0deb00c..., then the key should be rotated to a [k-of-n] multisig account.
Later, should the signatory list be updated, the auth key can be rotated again to support the new signatory schema.
Note that key rotation is required even without a vanity account, in case the multisig needs to be updated.
Here, ledger support will be required.

Package deployment

Once a vanity address account has been rotated to the multisig account, the account shall then deploy the Econia package.
Here, the signatories shall be able to use a ledger hardware wallet (for example via the Pontem wallet extension, or through a bespoke implementation) if they choose, otherwise a hot wallet.
Notably, the SIGN_TX command for ledger will need to be able to sign a transaction that has at its payload an entire Move package.
Similarly package upgrades (re-deployment) shall be supported.

Script invocation

Once the package has been deployed, the multisig wallet shall support Move script invocation, allowing either hot wallet or hardware wallet signing.
Scripts, rather than public entry functions, are required for several reasons:

  1. Scripts can be published on a public forum like GitHub, then then hash of the script can be verified before signing. This allows inspection of the actual commands to be run.
  2. One of the most common commands to run, update_incentives(), requires dozens of arguments, most of which are contained inside of a vector<vector<u64>>. Packing this vector can be easily inspected when its contents are defined in a Move script.
  3. Multiple maintenance operations will likely need to be run on a regular basis, including update_incentives(), set_recognized_markets() and remove_recognized_markets(), and it will be easier to track operations as well as achieve consensus via a Move scripts: only one transaction versus 3 required for consensus, and all operations are publicly inspectable.

@JackyWYX
Copy link

Here is some updates from MSafe:

Native smart contract Multi-sig implementation by Aptos

MSafe is planning to migrate to the new multi-sig implementation by Aptos team. The new implementation will have the following features:

  1. The new implementation will support owner change (add / remove owner, change threshold).
  2. The new implementation support migration from an existing account (both single signed & multi-ed25519).
  3. User will only need to sign once instead of twice.
  4. The new implementation only supports entry function calls ATM.

The current MSafe with multi-ed25519 implementation will be deprecated as the

We may analyze the features mentioned by Econia labs based on new / existing implementation.

Auth key rotation

  1. Currently, the auth key rotation is supported in the new multi-sig implementation. Including both add / remove owners, and migrating an account to the new multi-sig.
  2. Since we are deprecating msafe current implementation with multi-ed25519, the key rotation is not likely to be fully supported in this framework.

Package deployment

  1. Both existing MSafe and migrated MSafe solution will support deploying package with ledger support.
  2. The deployment with CLI is already live.
  3. The deployment with chrome extension will require developing a UI interface integrated with MSafe wallet, which shall take 3-4 weeks for development and polish.

Script invocation

  1. The new multi-sig implementation only supports entry function calls. If Econia labs has a strong initiate to implement the script function, it is suggested to communicate with Aptos team for this feature.

Our suggestion

  • Roll out the deployment with single ledger wallet.
  • Wait for Aptos roll out the next release
  • Migrate the single ledger wallet to multi-sig

@alnoki
Copy link
Member Author

alnoki commented Jan 27, 2023

@JackyWYX Thank you for all of the updates and suggestions. It is helpful that Aptos is extending its multisig support, but it looks like the feature is still under review and thus should not be expected for immediate prototyping. As for the script functionality, this would be a useful feature to have in the Aptos implementation: perhaps the script bytecode could be uploaded into a staging area of sorts and then everyone else could sign on-chain? As for standalone ledger versus multisig there is a bit of a trade-off here: using only a single ledger wallet reduces failure points, but multisig introduces the possiblity of hot wallet attacks (note that even if a hot wallet is compromised, however, the other signatories can still rotate out to a new multisig pubkey).

@JackyWYX
Copy link

Multiple wallets can also use pontem ledger :) Shall not be a concern.

@JackyWYX
Copy link

Another approach is to deploy with the current MSafe implementation, and wait for the new Aptos multi-sig & MSafe to come live. Once live, the current implementation can be migrated to the new implementation through a migration interface :)

@alnoki alnoki closed this as completed in af8e255 Jun 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants