Skip to content

Commit

Permalink
Merge pull request #2 from flow-hydraulics/access-checks
Browse files Browse the repository at this point in the history
Access checks
  • Loading branch information
whalelephant authored Aug 20, 2021
2 parents 8149a8c + 3dc2ac5 commit 4dd3315
Show file tree
Hide file tree
Showing 28 changed files with 684 additions and 157 deletions.
104 changes: 102 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,104 @@
<!-- markdownlint-configure-file { "MD013": { "line_length": 120 } } -->

# OnChainMultiSig

This repo is the source code for the `OnChainMultiSig` contract.
Its usage is demonstrated with the `MultiSigFlowToken` contract.
This repository is the source code for the `OnChainMultiSig` contract.

The `OnChainMultiSig` contract is designed to address the need for
multiple signature to authorise transactions without [time constraint] or [gas balance].

## Motivation

The motivation for this contract is in three parts:

1. **Limited Time Constraint** Natively, Flow suports multiple signers to authorise account transaction
with the innovative [`Weighted Keys`] in the [`Accounts`] system.
However, transactions all have expiration window (measured in blocks).
This is about [10 minutes] on the Mainnet.
It may not always be feasible or user friendly to require multisig key holder to be
present to sign everytime a transaction is required.

2. **Standardisation** It is common for an account to have multiple resources in their storage path and that
these resources all require the multisig feature. It will be easier for frontend developers or users to compose
signatures that captures the intention of the signer securely.

3. **Gas requirement** It should not be required that the signers of a multisig resource must
all have balance in an account, the signatures themselves should be enough to authorise
transactions of which some other [`payer`] (or themselves) can pay for.

## Solution

To address the time constraint, as the name suggests, the signatures are temporarily stored on chain.
Once all required signatures are ready, anyone can call the public method to execute the transaction.

Interfaces are defined to facilitate standardising this onchain signature storage across different resources.

Finally, following Flow's decoupling principle between account and keys,
the signature for a multisig transaction can be sumbitted by a trusted [`payer`],
independent to the account that owns the resource[^1] or the key.
This allows signers to simply be some entity that holds some private key where the corresponding public
key has been added as part of the resource's multisig public key list with some weight.

### Method Details

`OnChainMultiSig` contract provides a `Manager` resource which is intended to be created and stored by resources that
supports onchain multisig.
In addition, `PublicSigner` interface is provided for the resources as a standard interface for transactions to:

1. `addNewPayload`: Create a new payload and signature for it to be stored.
The `TxIndex` must be the current index incremented by one and is included in the signature.
Signature must be produced by the public key in `@Manager.keyList`
2. `addPayloadSignature`: Submit a signature for a payload that was added.
Signature must be produced by the public key in `@Manager.keyList`
3. `executeTx`: Execute a transaction (if all signatures required have been submitted)

and queries for:

1. `UUID`: gets the uuid of the multisig resource
2. `getTxIndex`: gets the sequentially assigned current txIndex of multisig pending tx of this resource
3. `getSignerKeys`: gets the list of public keys for the resource's multisig signers
4. `getSignerKeyAttr`: gets the stored key attributes

Internal to the `Manager` resource, it implements the `SignatureManager` interface which allows the implementation of `PublicSigner`
functions on the multisig supported resources to work with the `Manager`.

#### Usage

We will use a simple `Vault` resource in the `MultiSigFlowToken` contract to demonstrate the usage of the `PublicSigner`,
how to form a [onchain-multisig signature],
transacting with the `MultiSigFlowToken` contract and [resource owner account management].

TODO code walk through

### Signatures

The message in the signature verified by the `Manager` resource are as such:
**TODO details**

- `OnChainMultiSig.PayloadDetails`
- only a few types are supported at the moment
- only certain `signatureAlgorithm` and `hashAlgorithm` are supported at the moment

### Resource Owner Account Management

Whilst it is possible to allow for onchain multisig feature to be available for resources,
to limit the use to *just* be in that way will ultimately depend on the account that owns the resources.

As such, the account with such a resource should itself have all the keys added so that the `weights`
of each authorizer is consistent for the resource and the account. This is because if one key
is added to the account, that key has the ability to directly call functions in `Manager` to alter the states.

Another approach may be that once the resource has been added, all keys for the owner account is revoked.
This limits the flexibility of the account but it may be neccessary, similar to [immutable contracts] in Flow.

[onchain-multisig signature]: (#signatures)
[immutable contracts]: <https://docs.onflow.org/concepts/accounts-and-keys/#account-creation>
[decoupled]: <https://docs.onflow.org/concepts/accounts-and-keys/#account-creation>
[10 minutes]: <https://docs.onflow.org/flow-go-sdk/building-transactions/#reference-block>
[`payer`]: <https://docs.onflow.org/flow-go-sdk/building-transactions/#payer>
[gas balance]: <https://docs.onflow.org/flow-go-sdk/building-transactions/#payer>
[time constrains]: <https://docs.onflow.org/flow-go-sdk/building-transactions/#reference-block>
[`Accounts`]: <https://docs.onflow.org/concepts/accounts-and-keys/#accounts>
[`Weighted Keys`]: <https://docs.onflow.org/concepts/accounts-and-keys/#weighted-keys>
[resource owner account management]: (#resource-owner-account-management)
[^1]: Please see [resource owner account management] for details
76 changes: 41 additions & 35 deletions contracts/MultiSigFlowToken.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ pub contract MultiSigFlowToken: FungibleToken {
FungibleToken.Receiver,
FungibleToken.Balance,
OnChainMultiSig.PublicSigner,
OnChainMultiSig.PrivateKeyManager {
OnChainMultiSig.KeyManager {

// holds the balance of a users tokens
pub var balance: UFix64

// initialize the balance at resource creation time
init(balance: UFix64) {
self.balance = balance;
self.signatureStore = OnChainMultiSig.SignatureStore(publicKeys: [], pubKeyAttrs: []);
self.multiSigManager <- OnChainMultiSig.createMultiSigManager(publicKeys: [], pubKeyAttrs: [])
}


Expand All @@ -53,48 +53,37 @@ pub contract MultiSigFlowToken: FungibleToken {
vault.balance = 0.0
destroy vault
}

// PublicSigner interface requirements
// 1. signatureStore: Stores the payloads, transactions pending to be signed and signature
// 2. addNewPayload: add new transaction payload to the signature store waiting for others to sign
// 3. addPayloadSignature: add signature to store for existing paylaods by payload index
// 4. executeTx: attempt to execute the transaction at a given index after required signatures have been added
// 5. UUID: gets the uuid of this resource
// Interfaces 1-3 uses `OnChainMultiSig.Manager` struct for code implementation
// Interface 4 needs to be implemented specifically for each resource
/// struct to keep track of partial sigatures
pub var signatureStore: OnChainMultiSig.SignatureStore;

/// To submit a new paylaod, i.e. starting a new tx requiring more signatures
//
// Below resource and interfaces are required for any resources wanting to use OnChainMultiSig
//
// Resource to keep track of partial sigatures and payloads, required for onchain multisig features.
// Limited to `access(self)` to avoid exposing all functions in `SignatureManager` interface to account owner(s)
access(self) let multiSigManager: @OnChainMultiSig.Manager;

/// To submit a new paylaod, i.e. starting a new tx requiring, potentially requiring more signatures
pub fun addNewPayload(payload: OnChainMultiSig.PayloadDetails, publicKey: String, sig: [UInt8]) {
let manager = OnChainMultiSig.Manager(sigStore: self.signatureStore);
let newSignatureStore = manager.addNewPayload(resourceId: self.uuid, payload: payload, publicKey: publicKey, sig: sig);
self.signatureStore = newSignatureStore
self.multiSigManager.addNewPayload(resourceId: self.uuid, payload: payload, publicKey: publicKey, sig: sig);
}

/// To submit a new signature for a pre-exising payload, i.e. adding another signature
pub fun addPayloadSignature (txIndex: UInt64, publicKey: String, sig: [UInt8]) {
let manager = OnChainMultiSig.Manager(sigStore: self.signatureStore);
let newSignatureStore = manager.addPayloadSignature(resourceId: self.uuid, txIndex: txIndex, publicKey: publicKey, sig: sig);
self.signatureStore = newSignatureStore
self.multiSigManager.addPayloadSignature(resourceId: self.uuid, txIndex: txIndex, publicKey: publicKey, sig: sig);
}
/// To execute the multisig transaction iff conditions are met
/// `configureKey` and `removeKey` functions can be used for all resources if see fit
/// other methods must be implemented to suit the particular resource
pub fun executeTx(txIndex: UInt64): @AnyResource? {
let manager = OnChainMultiSig.Manager(sigStore: self.signatureStore);
let exeDetails = manager.readyForExecution(txIndex: txIndex) ?? panic ("no transactable payload at given txIndex")
let p = exeDetails.payload
self.signatureStore = exeDetails.signatureStore
let p = self.multiSigManager.readyForExecution(txIndex: txIndex) ?? panic ("no transactable payload at given txIndex")
switch p.method {
case "configureKey":
let pubKey = p.args[0] as? String ?? panic ("cannot downcast public key");
let weight = p.args[1] as? UFix64 ?? panic ("cannot downcast weight");
let newSignatureStore = manager.configureKeys(pks: [pubKey], kws: [weight])
self.signatureStore = newSignatureStore;
self.multiSigManager.configureKeys(pks: [pubKey], kws: [weight])
case "removeKey":
let pubKey = p.args[0] as? String ?? panic ("cannot downcast public key");
let newSignatureStore = manager.removeKeys(pks: [pubKey])
self.signatureStore = newSignatureStore;
self.multiSigManager.removeKeys(pks: [pubKey])
case "withdraw":
let amount = p.args[0] as? UFix64 ?? panic ("cannot downcast amount");
return <- self.withdraw(amount: amount);
Expand All @@ -116,20 +105,37 @@ pub contract MultiSigFlowToken: FungibleToken {
return self.uuid;
};

pub fun getTxIndex(): UInt64 {
return self.multiSigManager.txIndex
}

pub fun getSignerKeys(): [String] {
return self.multiSigManager.getSignerKeys()
}
pub fun getSignerKeyAttr(publicKey: String): OnChainMultiSig.PubKeyAttr? {
return self.multiSigManager.getSignerKeyAttr(publicKey: publicKey)
}

//
// --- end of `OnChainMultiSig.PublicSigner` interfaces
//
//
// Optional Priv Capbilities for owner of the vault to add / remove keys `OnChainMultiSig.KeyManager`
//
// These follows the usual account authorization logic
// i.e. if it is an account with multiple keys, then the total weight of the signatures must be > 1000
pub fun addKeys( multiSigPubKeys: [String], multiSigKeyWeights: [UFix64]) {
let manager = OnChainMultiSig.Manager(sigStore: self.signatureStore);
let newSignatureStore = manager.configureKeys(pks: multiSigPubKeys, kws: multiSigKeyWeights)
self.signatureStore = newSignatureStore;
self.multiSigManager.configureKeys(pks: multiSigPubKeys, kws: multiSigKeyWeights)
}

pub fun removeKeys( multiSigPubKeys: [String]) {
let manager = OnChainMultiSig.Manager(sigStore: self.signatureStore);
let newSignatureStore = manager.removeKeys(pks: multiSigPubKeys)
self.signatureStore = newSignatureStore;
self.multiSigManager.removeKeys(pks: multiSigPubKeys)
}

destroy() {
MultiSigFlowToken.totalSupply = MultiSigFlowToken.totalSupply - self.balance
destroy self.multiSigManager
}
}

Expand Down
Loading

0 comments on commit 4dd3315

Please sign in to comment.