-
Notifications
You must be signed in to change notification settings - Fork 47
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
Comments
Some specs from Momentum Safe
Please let us know if there is anything that might help on our end :) |
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];
)
}
}
|
Signatory mutationPresently, 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 Package snippetsBootstrapper[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(×tamp::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.
}
} InitializationHere, OperationsOperations from the governance account will thus obtain a signature via 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 resolutionWhen a governance script is executed, it shall call |
These are some trade-offs I see in the two options. Would love to have more ideas about this topic. |
@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 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 |
Modifications
|
Multi-ED25519 approachGiven 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 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 Hence, required support for a multi-ED25519 approach:
|
@JackyWYX For package deployment and maintenance, the following functionalities shall be supported: Auth key rotationUpon deployment, it is proposed that a multisig account first be established under a vanity account address. Package deploymentOnce a vanity address account has been rotated to the multisig account, the account shall then deploy the Econia package. Script invocationOnce the package has been deployed, the multisig wallet shall support Move script invocation, allowing either hot wallet or hardware wallet signing.
|
Here is some updates from MSafe: Native smart contract Multi-sig implementation by AptosMSafe is planning to migrate to the new multi-sig implementation by Aptos team. The new implementation will have the following features:
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
Package deployment
Script invocation
Our suggestion
|
@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). |
Multiple wallets can also use pontem ledger :) Shall not be a concern. |
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 :) |
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 anAPT/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
forwBTC/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 inAPT
. 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:@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:@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
The text was updated successfully, but these errors were encountered: