Table of Contents
The Security Council uses a specially extended Safe multisig contract to ensure that any loss of access to a signer's keys is identified and addressed within a predictable period of time.
This mechanism is intended only to be used to remove signers who have lost access to their keys, or are otherwise inactive. It is not intended to be used to remove signers who are acting in bad faith, or any other subjective criteria, such cases should be addressed by governance, and the removal handled via the standard Safe ownership management functionality.
This is achieved using two types of contracts which the Safe contract has built-in support for:
- Guard contracts: can execute pre- and post- transaction checks.
- Module contracts: a contract which is added to the Safe by the signers, and thenceforth is authorized to execute transactions via the Safe. This means the module must properly implement auth conditions internally.
For implementing liveness checks a LivenessGuard
is created which receives the signatures from
each executed transaction, and tracks the latest time at which a transaction was signed by each
signer. This time is made publicly available by calling a lastLive(address)(Timestamp)
method.
Owners are recorded in this mapping in one of 4 ways:
- Upon deployment, the guard reads the current set of owners from the Safe contract.
- When a new owner is added to the safe. Similarly, when an owner is removed from the Safe, its entry is deleted from the mapping.
- When a transaction is executed, the signatures on that transaction are passed to the guard and used to identify the signers. If more than the required number of signatures is provided, they are ignored.
- An owner may call the contract's
showLiveness()()
method directly in order to prove liveness.
Note that the first two methods do not require the owner to actually sign anything. However these mechanisms are necessary to prevent new owners from being removed before they have had a chance to show liveness.
A LivenessModule
is also created which does the following:
- Has a function
removeOwners()
that anyone may call to specify one or more owners to be removed from the Safe. - The Module would then check the
LivenessGuard.lastLive()
to determine if the signer is eligible for removal. - If so, it will call the Safe's
removeSigner()
to remove the non-live signer, and if necessary reduce the threshold. - When a member is removed, the signing parameters are modified such that
M/N
is the lowest ratio which remains greater than or equal to 75%. Using integer math, this can be expressed asM = (N * 75 + 99) / 100
.
The following diagram illustrates the flow for removing a single owner. The verifyFinalState
box indicates calls to the Safe which ensure the final state is valid.
sequenceDiagram
participant User
participant LivenessModule
participant LivenessGuard
participant Safe
User->>LivenessModule: removeOwners([previousOwner], [owner])
LivenessModule->>LivenessGuard: lastLive(owner)
LivenessModule->>Safe: getOwners()
LivenessModule->>Safe: removeOwner(previousOwner, owner)
alt verifyFinalState
LivenessModule->>Safe: getOwners()
LivenessModule->>Safe: getThreshold()
LivenessModule->>Safe: getGuard()
end
In the unlikely event that the signer set (N
) is reduced below the allowed minimum number of
owners, then (and only then) is a shutdown mechanism activated which removes the existing
signers, and hands control of the multisig over to a predetermined entity.
The following security properties must be upheld:
- Signatures are assigned to the correct signer.
- Non-signers are unable to create a record of having signed.
- An owner cannot be censored or griefed such that their signing is not recorded.
- Owners may demonstrate liveness either by signing a transaction or by calling directly to the guard.
- It must be impossible for the guard's
checkTransaction
orcheckAfterExecution
method to permanently revert given any calldata and the current state. - The guard correctly handles updates to the owners list, such that new owners are recorded, and
removed owners are deleted.
- An
ownersBefore
enumerable set variable is used to accomplish this, it must be emptied at the end of thecheckAfterExecution
call.
- An
- During a shutdown the module correctly removes all signers, and converts the safe to a 1 of 1.
- The module only removes an owner if they have not demonstrated liveness during the interval, or if enough other owners have been removed to activate the shutdown mechanism.
- The module correctly sets the Safe's threshold upon removing a signer.
Note: neither the module nor guard attempt to prevent a quorum of owners from removing either the liveness module or guard. There are legitimate reasons they might wish to do so. Moreover, if such a quorum of owners exists, there is no benefit to removing them, as they are defacto 'sufficiently live'.
The guard has no dependency on the module, and can be used independently to track liveness of Safe owners.
This means that the module can be removed or replaced without any affect on the guard.
The module however does have a dependency on the guard; if the guard is removed from the Safe, then
the module will no longer be functional and calls to its removeOwners
function will revert.
As noted above newly added owners are recorded in the guard without necessarily having signed a transaction. Off-chain validation of the liveness of an address must therefore be done prior to adding a new owner.
The module and guard are intended to be deployed and installed on the safe in the following sequence:
- Deploy the guard contract 2. The guard's constructor will read the Safe's owners and set a timestamp
- Deploy the module.
- Set the guard on the safe.
- Enable the module on the safe.
This order of operations is necessary to satisfy the constructor checks in the module, and is intended to prevent owners from being immediately removable.
Note that changes to the owners set should not be made between the time the module is deployed, and when it is enabled on the Safe, otherwise the checks made in the module's constructor may be invalidated. If such changes are made, a new module should be deployed.
Changes to the liveness checking system should be done in the following manner:
The module can safely be removed without affecting the operation of the guard. A new module can then be added.
Note: none of the module's parameters are modifiable. In order to update the security properties enforced by the module, it must be replaced.
The safe can only have one guard contract at a time, and if the guard is removed the module will cease to function. This does not affect the ability of the Safe to operate normally, however the module should be removed as a best practice.
If a new guard is added, eg. as a means of upgrading it, then a new module will also need to be deployed and enabled. Once both the guard and module have been removed, they can be replaced according to the steps in the Deployment section above.