# Actor Spec: Multisig [Back to Master Tracking Doc](https://hackmd.io/LOZjAsz-THelSD5lWqSVlw) ## Contents [TOC] ## At a Glance The Multisig actor is a single actor representing a group of Signers. Signers may be external users, other Multisigs, or even the Multisig itself. ## State ```go= type State struct { Signers []address.Address NumApprovalsThreshold uint64 NextTxnID TxnID InitialBalance abi.TokenAmount StartEpoch abi.ChainEpoch UnlockDuration abi.ChainEpoch PendingTxns cid.Cid } ``` **`Signers`**: A slice of addresses representing the Multisig's Signers. * Notes: * ALL address protocols allowed. * Each Signer should be unique. No entries within Signers should resolve to other entries. * Each Signer should have equivalent permissions to `Multisig.Propose` and `Multisig.Approve` within the Multisig. * Can be modified via `Multisig.AddSigner`, `Multisig.RemoveSigner`, and `Multisig.SwapSigner` * Invariants: * `len(Signers) != 0` **`NumApprovalsThreshold`**: The number of Signers that must Approve a Transaction in order for it to be executed. * Notes: * Can be modified via `ChangeNumApprovalsThreshold` * Invariants: * `NumApprovalsThreshold <= len(Signers)` * `NumApprovalsThreshold != 0` **`NextTxnID`**: The TxnID that will be assigned to the next proposed Transaction. * Notes: * `NextTxnID` should be equal to the total number of proposed Transactions in the Multisig's history. * Invariants: * `NextTxnID >= 0` **`InitialBalance`**: Denotes a TokenAmount that is locked in the Multisig until `UnlockDuration` epochs elapse. * Notes: * `InitialBalance` is unused unless an `UnlockDuration` is specified on construction. * `InitialBalance` is unlocked gradually over the `UnlockDuration` * Invariants: * `InitialBalance >= 0` **`StartEpoch`**: Denotes the epoch in which `InitialBalance` was locked for `UnlockDuration`. * Notes: * `StartEpoch` is unused unless an `UnlockDuration` is specfied on construction. * If set, `StartEpoch` is the CurrEpoch in which the Multisig was created. * Invariants: * `StartEpoch > 0` * `StartEpoch <= rt.CurrEpoch()` **`UnlockDuration`**: Denotes the number of epochs after `StartEpoch` at which point the full amount `InitialBalance` becomes available for use by the Multisig. * Notes: * Before `UnlockDuration` elapses, the Multisig should ensure that the correct portion of `InitialBalance` is not spent by any executed proposals. * Invariants: * `UnlockDuration >= 0` **`PendingTxns`**: Before Transactions are executed by the Multisig, they're stored in `PendingTxns`. * Notes: * Cid type: HAMT, `map[TxnID]Transaction` * Fully approved and executed Transactions are deleted from `PendingTxns` * If `NumApprovalsThreshold` decreases, `PendingTxns` may contain fully-approved but un-executed Transactions. * These Transactions should be executable via `Multisig.Approve`. #### Transaction Resolved from `state.PendingTxns` ```go= type Transaction struct { To addr.Address Value abi.TokenAmount Method abi.MethodNum Params []byte Approved []addr.Address } ``` **`To`**: The destination of the Send. * Notes: * ALL address protocols allowed. * No restrictions currently exist for this value. * If `To == Receiver`, the Multisig will `Send` to itself. This is used to access functions like `Multisig.AddSigner`, `Multisig.RemoveSigner`, `Multisig.SwapSigner`, and `Multisig.ChangeNumApprovalsThreshold`. **`Value`**: The TokenAmount that will be provided via Send. * Notes: * Whether the Multisig has `Value` available to Send is checked when the Transaction is fully approved for execution. * Invariants: * `Value >= 0` **`Method`**: The MethodNum that will be invoked via Send. * Notes: * No restrictions currently exist for this value. **`Params`**: The parameters that will be provided to the invoked method via Send. * Notes: * No restrictions currently exist for this value. **`Approved`**: A slice of addresses that have registered approval for this Transaction. * Notes: * The address at index 0 is the Proposer of the Transaction. Only this address may Cancel the pending Transaction before its execution. * Invariants: * `len(Approved) >= 1` ## Exported Methods #### 1. Constructor ```go= func (a Actor) Constructor(rt vmr.Runtime, params *ConstructorParams) *adt.EmptyValue ``` Initializes the Multisig's `State` with a slice of Signers and a threshold approval count before proposals may be passed. Additionally, if `params.UnlockDuration` is nonzero, any `ValueReceived` vests over a period of time denoted by `params.StartEpoch` and `params.UnlockDuration`. ##### Parameters ```go= type ConstructorParams struct { Signers []addr.Address NumApprovalsThreshold uint64 UnlockDuration abi.ChainEpoch StartEpoch abi.ChainEpoch } ``` **`Signers`**: A slice of addresses to initialize as the Multisig's `Signers` * Requirements: * `len(Signers) != 0 && len(Signers) <= SignersMax` (256) * All resolved addresses in `Signers` must be unique. **`NumApprovalsThreshold`**: The number of `Signers` that must call `Approve` in order to execute a Transaction * Requirements: * `NumApprovalsThreshold <= len(Signers)` * `NumApprovalsThreshold != 0` **`UnlockDuration`**: If this value is nonzero, specifies that any `ValueReceived` vests linearly over a period of epochs `UnlockDuration`. * Requirements: * `UnlockDuration >= 0` **`StartEpoch`**: If `UnlockDuration` is nonzero, the `CurrentBalance` of the multisig vests linearly over a period of epochs starting at `StartEpoch` and lasting `UnlockDuration` epochs. ##### Failure conditions * `params.Signers` was empty, or contained duplicate members * `params.NumApprovalsThreshold` was greater than the number of supplied Signers, or was 0 * `params.UnlockDuration` was negative. #### 2. Propose ```go= func (a Actor) Propose(rt vmr.Runtime, params *ProposeParams) *ProposeReturn ``` One of the Multisig's Signers proposes a Transaction to be executed. * The Caller's approval for the newly pending Transaction is registered as well. * If the newly pending Transaction already has enough approvals for execution, it is executed. ##### Parameters ```go= type ProposeParams struct { To addr.Address Value abi.TokenAmount Method abi.MethodNum Params []byte } ``` **`To`**: The destination of the Send. * Notes: * ALL address protocols allowed. * No restrictions currently exist for this value. * If `To == Receiver`, the Multisig will `Send` to itself. This is used to access functions like `Multisig.AddSigner`, `Multisig.RemoveSigner`, `Multisig.SwapSigner`, and `Multisig.ChangeNumApprovalsThreshold`. **`Value`**: The TokenAmount that will be provided via Send. * Notes: * Whether the Multisig has `Value` available to Send is checked when the Transaction is fully approved for execution. * Requirements: * `Value >= 0` **`Method`**: The MethodNum that will be invoked via Send. * Notes: * No restrictions currently exist for this value. **`Params`**: The parameters that will be provided to the invoked method via Send. * Notes: * No restrictions currently exist for this value. ##### Return ```go= type ProposeReturn struct { TxnID TxnID Applied bool Code exitcode.ExitCode Ret []byte } ``` **`TxnID`**: The `TxnID` associated with the newly-proposed Transaction. * Notes: * Should be equal to `st.NextTxnID` before being incremented. * Invariants: * `TxnID >= 0` **`Applied`**: Whether or not the proposal was executed. * Notes: * This should only be the case if `NumApprovalsThreshold == 1`, as it's only possible to have 1 Approval on a newly-proposed Transaction. **`Code`**: If the Transaction was Applied, `Code` is the ExitCode of the Send. * Notes: * This value only matters if `Applied == true` **`Ret`**: If the Transaction was Applied, `Ret` is the return data of the Send. * Notes: * This value only matters if `Applied == true` ##### Failure conditions * Caller is not a Signable type actor * Caller is not a Signer in the Multisig * Specified `Value` to send is negative #### 3. Approve ```go= func (a Actor) Approve(rt vmr.Runtime, params *TxnIDParams) *ApproveReturn ``` One of the Multisig's Signers marks their approval for a specific pending Transaction. `Approve` follows roughly these steps: * If the transaction has sufficient approvals BEFORE the Caller's approval is registered, the Transaction is executed. * Then, the Caller's approval is registered. * Finally, if the Transaction has sufficient approvals AFTER the Caller's approval is registered, the Transaction is executed. In the case that some Transaction `t` has reached the `NumApprovalThreshold` but has not yet been executed, the above order allows a Signer that has already approved `t` to invoke `Approve(t)` to execute the Transaction. ##### Parameters ```go= type TxnIDParams struct { ID TxnID ProposalHash []byte } ``` **`ID`**: The TxnID of the pending Transaction. * Requirements: * Must correspond to an existing Transaction in `st.PendingTxns` **`ProposalHash`**: OPTIONAL. If not `nil`, this is compared to the hash of the Transaction referenced by `ID`. If the two hashes do not match, execution aborts. * Notes: * This is primarily used so the Caller can validate that the TxnID they provide is referencing the expected Transaction. (In case of re-org) ##### Return ```go= type ApproveReturn struct { Applied bool Code exitcode.ExitCode Ret []byte } ``` **`Applied`**: Whether or not the proposal was executed. * Notes: * If `true`, the other fields in the Return provide information about the execution. **`Code`**: If the Transaction was Applied, `Code` is the ExitCode of the Send. * Notes: * This value only matters if `Applied == true` **`Ret`**: If the Transaction was Applied, `Ret` is the return data of the Send. * Notes: * This value only matters if `Applied == true` ##### Failure conditions * Caller is not a Signable type actor * Caller is not a Signer in the Multisig * Referenced `ID` does not exist in `st.PendingTxns` * `ProposalHash` is not `nil` AND does not match calculated hash of referenced Transaction. * Caller has already approved the Transaction AND there are not enough approvals for execution * Enough approvals exist for execution, but the Multisig does not have the available balance for the Transaction's specified `Value` #### 4. Cancel ```go= func (a Actor) Cancel(rt vmr.Runtime, params *TxnIDParams) *adt.EmptyValue ``` Allows the original proposer of a Transaction to cancel its pending status, after which it cannot be approved or executed. * The "original proposer" of a Transaction is the first `Address` in `Transaction.Approved` * Once cancelled, the pending Transaction is completely deleted from `st.PendingTxns`. * Transactions can only be cancelled once. ##### Parameters ```go= type TxnIDParams struct { ID TxnID ProposalHash []byte } ``` **`ID`**: The TxnID of the pending Transaction. * Requirements: * Must correspond to an existing Transaction in `st.PendingTxns` **`ProposalHash`**: OPTIONAL. If not `nil`, this is compared to the hash of the Transaction referenced by `ID`. If the two hashes do not match, execution aborts. * Notes: * This is primarily used so the Caller can validate that the TxnID they provide is referencing the expected Transaction. (In case of re-org) ##### Failure conditions * Caller is not a Signable type actor * Caller is not a Signer in the Multisig * Referenced `ID` does not exist in `st.PendingTxns` * Caller is not the Proposer of the Transaction in question. * (`st.PendingTxns[ID].Approved[0] != Caller`) * `ProposalHash` is not `nil` AND does not match calculated hash of referenced Transaction. #### 5. AddSigner ```go= func (a Actor) AddSigner(rt vmr.Runtime, params *AddSignerParams) *adt.EmptyValue ``` Provides the Multisig functionality to add a Signer to `st.Signers`. * Can only be invoked by the Multisig itself, meaning this is only reachable through an approved and executed Transaction. * Optionally, may increment `st.NumApprovalsThreshold` by 1. ##### Parameters ```go= type AddSignerParams struct { Signer addr.Address Increase bool } ``` **`Signer`**: The address that will be added to `st.Signers`. * Notes: * ALL address protocols allowed. * Requirements: * `Signer` must not exist in `st.Signers` already. **`Increase`**: If `true`, `st.NumApprovalsThreshold` will be increased by 1. ##### Failure conditions * Caller is not `Message().Receiver()` (the Multisig itself) * `params.Signer` is already a Signer * The additional Signer increases the total number of Signers beyond `SignersMax` (256) #### 6. RemoveSigner ```go= func (a Actor) RemoveSigner(rt vmr.Runtime, params *RemoveSignerParams) *adt.EmptyValue ``` Provides the Multisig functionality to remove a Signer from `st.Signers`. * Can only be invoked by the Multisig itself, meaning this is only reachable through an approved and executed Transaction. * Optionally, may decrement `st.NumApprovalsThreshold` by 1. * Only succeeds if the Multisig currently has more than 1 Signer. ##### Parameters ```go= type RemoveSignerParams struct { Signer addr.Address Decrease bool } ``` **`Signer`**: The address that will be removed from `st.Signers`. * Notes: * ALL address protocols allowed. * Requirements: * `Signer` must exist in `st.Signers` **`Decrease`**: If `true`, `st.NumApprovalsThreshold` will be decreased by 1. * Requirements: * If removing `Signer` would result in `len(st.Signers) < st.NumApprovalsThreshold`, `Decrease` must be `true` ##### Failure conditions * Caller is not `Message().Receiver()` (the Multisig itself) * `params.Signer` is not a Signer * There is only 1 Signer in the Multisig * (`len(st.Signers) == 1`) * `params.Decrease` is `false` AND the removal of `params.Signer` would result in `len(st.Signers) < st.NumApprovalsThreshold` #### 7. SwapSigner ```go= func (a Actor) SwapSigner(rt vmr.Runtime, params *SwapSignerParams) *adt.EmptyValue ``` Provides the Multisig functionality to atomically remove one Signer AND add a new Signer. * Can only be invoked by the Multisig itself, meaning this is only reachable through an approved and executed Transaction. * Successful execution should leave the number of Signers unchanged. ##### Parameters ```go= type SwapSignerParams struct { From addr.Address To addr.Address } ``` **`From`**: The address that will be removed from `st.Signers`. * Notes: * ALL address protocols allowed. * Requirements: * `From` must already be a Signer **`To`**: The address that will be added to `st.Signers` * Notes: * ALL address protocols allowed. * Requirements: * `To` must not already be a Signer ##### Failure conditions * Caller is not `Message().Receiver()` (the Multisig itself) * `params.From` is not a Signer * `params.To` is already a Signer #### 8. ChangeNumApprovalsThreshold ```go= func (a Actor) ChangeNumApprovalsThreshold(rt vmr.Runtime, params *ChangeNumApprovalsThresholdParams) *adt.EmptyValue ``` Provides the Multisig functionality to adjust the number of Signers needed to pass proposals. * Can only be invoked by the Multisig itself, meaning this is only reachable through an approved and executed Transaction. ##### Parameters ```go= type ChangeNumApprovalsThresholdParams struct { NewThreshold uint64 } ``` **`NewThreshold`**: The new `NumApprovalsThreshold` * Requirements: * `NewThreshold != 0` * `NewThreshold <= len(st.Signers)` ##### Failure conditions * Caller is not `Message().Receiver()` (the Multisig itself) * `params.NewThreshold == 0` * `params.NewThreshold > len(st.Signers)` ## Open Questions * If a Signer is added, should they immediately be able to Approve prior `PendingTxns`? * If a Signer is removed, should the `PendingTxns` they approved reflect this by removing the Signer from `Approved`? * If `NumApprovalsThreshold` is changed, should prior `PendingTxns` use the updated value? * Should it be possible to `Propose` a transaction, even if it sends more value than is currently available to the multisig? * Should it be possible to `Approve` a transaction, even if it sends more value than is currently available to the multisig? * If a proposal meets the approval threshold (but has not yet been executed), should the proposal be able to be cancelled? * Is Multisig.SwapSigner intended specifically for the scenario where a Signer wishes to use a different public key? * Should Multisig.ChangeNumApprovalsThreshold fail if the new threshold is the same as the old? ## Questions * Given that actor state is loaded all-at-once, is it possible that this object may get too large to load (with arbitrarily-large `Signers`)? * anorth: Yes. * Does a `Signer` need to exist on chain before being added to a multisig's `Signers`? * anorth: No. * Can a multisig be one of its own `Signers`? * anorth: Yes. * If the original proposer is removed from the multisig, can the proposal be cancelled? * anorth: No (but convince me we should change that) * Can a multisig send value without executing a method? What would `Method` be in this case? * anorth: method number 0 is a pure value send