For the complete documentation index, see llms.txt. This page is also available as Markdown.

28. Install the recovery module and manage guardians

Installing the recovery module gives an identity a way back when its MANAGEMENT keys are lost. The Identity Owner installs the RecoveryModule with the guardians it chose, a threshold, a delay and an expiration, and gives the module the authority to change keys. It then keeps the guardian set current. All of this takes a MANAGEMENT key, so it is done while the owner still has its keys, ideally in the same session as the creation of the identity.

At a glance

Triggered by

Identity Owner

Actors

Identity Owner, Guardian, Platform

Contracts

ONCHAINID, RecoveryModule, ERC734Validator

Preconditions

The identity exists (see 02); the owner controls a MANAGEMENT key of the identity (see 22); the address of the RecoveryModule is known: one module serves every identity; each guardian has agreed and has given its signer data

Result

The module is installed on the identity with its guardians, weights, threshold, delay and expiration, and holds the MANAGEMENT purpose

Actors and roles

Actor
Role in this flow
What they hold

Identity Owner

Chooses the guardians and the settings, installs the module and maintains it.

A MANAGEMENT key of the identity

Guardian

Agrees to act for the owner and provides the signer it signs with. Does not transact in this flow.

A signer of its own. No key of the identity

Platform

Builds the installation data, prepares the calls and shows the settings back to the owner.

Whatever keys the owner has authorized

Sequence

Step by step

Step 1. Choose the guardians and the settings

Who: Identity Owner, with the Platform

Call: Off-chain

Before the call: The owner has decided who its guardians are, and each guardian has given the signer it signs with.

What happens: The owner fixes five settings.

Setting
Meaning

Guardians

The signers that can authorize a recovery together. Each is given as signer data: for an address, its 20 bytes.

Weights

The weight of each guardian's signature, one for each guardian and at least 1. One each, unless the owner wants a guardian to count for more.

Threshold

The total weight a recovery or a cancellation by guardians must reach. For example, 2 with three guardians of weight 1.

Delay

The time between the scheduling of a recovery and the moment it can execute. The owner can cancel during this time.

Expiration

The time a recovery stays executable after its delay. Past it, the recovery has to be scheduled again.

The Platform encodes them as the installation data, initData: the two-byte length of the executor arguments, the executor arguments abi.encodePacked(uint32 delay, uint32 expiration), the two-byte length of the guardian arguments, and the guardian arguments abi.encode(bytes[] guardians, uint64 threshold, uint64[] weights).

What changes: Nothing on-chain.

Step 2. Install the module

Who: Identity Owner (MANAGEMENT key)

Call: Identity.installModule(2, recoveryModule, initData), where 2 is the ERC-7579 module type of an executor

Before the call: Step 1 is done. The Platform shows the owner the guardians and the settings it is about to install.

What the contract checks:

  1. The caller holds MANAGEMENT on the identity, or is the identity itself. Otherwise the call reverts with SenderDoesNotHaveManagementKey.

  2. The module declares itself an executor. Otherwise ERC7579MismatchedModuleTypeId.

  3. The module is not already installed on the identity. Otherwise ERC7579AlreadyInstalledModule.

  4. In the module, the identity has no settings yet. Otherwise ERC7579DelayedExecutorAlreadyInstalled or ERC7579MultisigAlreadyInstalled.

  5. The guardian arguments decode and list at least one guardian. Otherwise ERC7579MultisigInvalidInitData or ERC7579MultisigEmptySigners.

  6. Every guardian is at least 20 bytes long and is listed once. Otherwise ERC7579MultisigInvalidSigner or ERC7579MultisigAlreadyExists.

  7. There is one weight for each guardian, and no weight is zero. Otherwise ERC7579MultisigMismatchedLength or ERC7579MultisigInvalidWeight.

  8. The threshold is above zero and within the total weight of the guardians. Otherwise ERC7579MultisigZeroThreshold or ERC7579MultisigUnreachableThreshold.

What changes: The identity records the module as one of its executors and emits ModuleInstalled, then ModuleInstallData(moduleTypeId, module, initData, caller). The module stores the guardians, the weights, the threshold, the delay and the expiration for this identity, and emits ERC7913SignerAdded for each guardian, ERC7579MultisigWeightChanged for each weight above 1, ERC7913ThresholdSet, ERC7579ExecutorDelayUpdated and ERC7579ExecutorExpirationUpdated. The module cannot change a key yet.

Step 3. Give the module the authority to change keys

Who: Identity Owner (MANAGEMENT key)

Call: Identity.addKeyWithData(keyHash, 1, 4, signerData, clientData), with signerData the 20 bytes of the module's address, keyHash = keccak256(signerData), purpose 1 for MANAGEMENT, key type 4 for MODULE and an empty clientData

Before the call: Step 2 is confirmed.

What the contract checks: As for any key, in 22: the caller holds MANAGEMENT, the key hash matches the signer data, and the module does not already hold the purpose. Otherwise SenderDoesNotHaveManagementKey, InvalidSignerData or KeyAlreadyRegistered.

What changes: The key registry records the module as a key of type MODULE with the purpose MANAGEMENT, and emits KeyAdded(identity, keyHash, purpose, keyType, signerData, clientData, caller). A recovery executed by the module is now allowed to add a MANAGEMENT key to the identity. A key of type MODULE cannot sign, and it does not count as a manager when the identity checks that a MANAGEMENT signer remains.

Step 4. Confirm the settings

Who: Platform

Call: Off-chain, using read calls

Before the call: Steps 2 and 3 are confirmed.

What happens: The Platform reads Identity.isModuleInstalled(2, recoveryModule, "") and Identity.keyHasPurpose(keyHash, 1), which must both be true. On the module, it reads getGuardians(identity, start, end), getGuardianCount(identity), signerWeight(identity, guardian) for each guardian, totalWeight(identity), threshold(identity), getDelay(identity) and getExpiration(identity), and shows them to the owner. The owner tells each guardian that it is now a guardian of this identity, and how a request for recovery reaches it.

What changes: Nothing on-chain.

Step 5. Change the guardians or the settings

Later, each time a guardian has to be replaced or a setting adjusted.

Who: Identity Owner (MANAGEMENT key)

Call: Identity.execute(identity, 0, data). data is a call of the identity to its own ERC-7579 execute(mode, executionCalldata), and executionCalldata targets the module with one of: addGuardians(guardians), removeGuardians(guardians), setSignerWeights(guardians, weights), setThreshold(threshold), setDelay(delay), setExpiration(expiration)

Before the call: The owner has decided the change. For a new guardian, the guardian has agreed and given its signer data. The Platform reads the current settings, as in step 4.

What the contract checks:

  1. The request comes from a MANAGEMENT key. The identity's execution queue then runs it at once: the identity calls itself, and the module sees the identity as its caller. A request queued by a key that does not hold MANAGEMENT is recorded and not run.

  2. The module changes the settings of its caller, and of no one else. Only a call that comes from the identity changes the identity's settings. The identity refuses to dispatch a call into one of its own modules unless it is calling itself: OwnModuleTargetBlocked.

  3. In the module: a guardian to add is at least 20 bytes long and not already listed (ERC7579MultisigInvalidSigner, ERC7579MultisigAlreadyExists). A guardian to remove or to reweight is listed (ERC7579MultisigNonexistentSigner). A weight is not zero (ERC7579MultisigInvalidWeight). After the change, the threshold is above zero and within the total weight (ERC7579MultisigZeroThreshold, ERC7579MultisigUnreachableThreshold).

A failure of check 2 or 3 does not revert the owner's transaction. The queue catches it: the transaction succeeds, the identity's queue emits ExecutionFailed, and nothing changes. The error of the module is not returned to the caller.

What changes: The identity's queue emits Executed, and the module updates the settings of this identity. A change of guardians, of weights, of threshold or of expiration applies at once. A change of delay does not. A longer delay takes effect after the new delay itself, capped at 5 days. A shorter delay takes effect after the difference between the current delay and the new one, so a recovery can never execute sooner than the delay that applied when the change was made. Until then, getDelay(identity) returns the current delay, the pending one and the time it takes effect.

State after the flow

  • Identity.isModuleInstalled(2, recoveryModule, "") is true.

  • Identity.keyHasPurpose(keyHash, 1) is true for the module's key.

  • RecoveryModule.getGuardianCount(identity), totalWeight(identity), threshold(identity), getDelay(identity) and getExpiration(identity) return the settings the owner chose.

  • RecoveryModule.isGuardian(identity, guardian) is true for each guardian.

  • The identity's own keys, claims, linked wallets and positions are unchanged.

  • No recovery is scheduled. The guardians have no authority until they sign one together.

Failure modes

Symptom
Cause
Fix

installModule reverts with SenderDoesNotHaveManagementKey

The caller does not hold MANAGEMENT on the identity (step 2, check 1)

Submit with a MANAGEMENT key

installModule reverts with ERC7579MismatchedModuleTypeId

The module does not declare the module type that was passed (step 2, check 2)

Use the address of a RecoveryModule and module type 2

installModule reverts with ERC7579AlreadyInstalledModule

The module is already installed on this identity (step 2, check 3)

Change its settings with step 5

installModule reverts with ERC7579DelayedExecutorAlreadyInstalled or ERC7579MultisigAlreadyInstalled

The module already holds settings for this identity (step 2, check 4)

Change them with step 5

installModule reverts with ERC7579MultisigInvalidInitData or ERC7579MultisigEmptySigners

The guardian arguments are malformed, or list no guardian (step 2, check 5)

Rebuild initData as in step 1

installModule reverts with ERC7579MultisigInvalidSigner or ERC7579MultisigAlreadyExists

A guardian is shorter than 20 bytes, or listed twice (step 2, check 6)

Pass each guardian once, as signer data

installModule reverts with ERC7579MultisigMismatchedLength or ERC7579MultisigInvalidWeight

The weights do not match the guardians one for one, or a weight is zero (step 2, check 7)

Give each guardian a weight of at least 1

installModule reverts with ERC7579MultisigZeroThreshold or ERC7579MultisigUnreachableThreshold

The threshold is zero, or above the total weight (step 2, check 8)

Choose a threshold between 1 and the total weight

addKeyWithData reverts with InvalidSignerData

keyHash is not the hash of the module's 20-byte address (step 3)

Compute keyHash = keccak256(abi.encodePacked(recoveryModule))

addKeyWithData reverts with KeyAlreadyRegistered

The module already holds MANAGEMENT (step 3)

Nothing to do

A recovery executes and reverts with ExecutorPurposeNotAuthorized

The module was installed and step 3 was never done

Do step 3 while a MANAGEMENT key exists. Check it in step 4

A change of settings has no effect and no Executed event appears

The request was queued by a key that does not hold MANAGEMENT (step 5, check 1)

Submit with a MANAGEMENT key

The transaction succeeds, ExecutionFailed is emitted and the settings are unchanged

The call targeted the module directly and not through the identity calling itself (step 5, check 2), or the module refused the change, for example a removal after which the threshold cannot be reached (step 5, check 3)

Wrap the call as in step 5. Lower the threshold first, or add the replacement before removing

A user operation that targets the module reverts with OwnModuleTargetBlocked

The identity does not dispatch a call into its own modules (step 5, check 2)

Make the identity call itself, as in step 5

A new delay does not apply

A change of delay waits for its transition period (step 5)

Read getDelay(identity) for the pending value and its effect time

Common misunderstandings

Wrong: "Installing the module is enough."

Right: Installation stores the guardians and the settings. The module can only add a key to the identity once it holds the MANAGEMENT purpose itself, as a key of type MODULE. Without step 3, a recovery is scheduled, waits for its delay, and fails when it executes.

Wrong: "Guardians can replace one another."

Right: The guardian set and the settings change only by a call from the identity itself, which takes a MANAGEMENT key. Guardians can do two things together: authorize an operation that executes after the delay, and cancel a scheduled one.

Wrong: "The module is one more manager of the identity."

Right: A key of type MODULE cannot sign anything. It lets the module execute what the guardians authorized, after the delay. It does not count when the identity checks that a MANAGEMENT signer remains.

Wrong: "A guardian has to be an ONCHAINID."

Right: A guardian is a signer: an address, or any other signer type the identity layer verifies. It needs no identity, no key on the protected identity and no role anywhere.

Integration notes

  • Offer this flow in the same session as the creation of the identity (02). Steps 2 and 3 can be sent together. An identity that loses its keys before step 3 has no on-chain recovery.

  • Always run step 4, and show the owner the result. A module installed without the MANAGEMENT purpose looks configured and cannot recover anything.

  • One RecoveryModule serves every identity, like every module of the identity layer. It keeps its settings per identity, and a guardian's signature is bound to the identity it was made for.

  • When the executor arguments are left out of the installation data, the module applies its defaults: a delay of 5 days and an expiration of 60 days.

  • Choose the delay with the owner. It is the time the owner has to cancel a recovery it did not ask for. Monitor scheduled recoveries on every identity your application serves.

  • Guardians are stored as signer data. For an address, pass abi.encodePacked(address), in the installation data and in every later call.

  • Replace a guardian by adding the new one before removing the old one, so that the threshold stays reachable at every step.

  • After each change, read the settings again as in step 4. A change that the module refused does not revert the transaction: it shows as ExecutionFailed on the identity's queue.

  • After a recovery, review the guardians: the event that cost the keys may have exposed them too.

  • Uninstalling the module with Identity.uninstallModule(2, recoveryModule, "") removes the purposes the module holds and its guardian settings for the identity. The identity then has no on-chain recovery until the module is installed again. The delay is not forgotten at once: it winds down over its own length, and an installation made in the meantime follows the transition rules of step 5, so uninstalling and reinstalling cannot shorten the delay at once.

  • Index the module's events, filtered by identity, to follow a configuration: ERC7913SignerAdded, ERC7913SignerRemoved, ERC7579MultisigWeightChanged, ERC7913ThresholdSet, ERC7579ExecutorDelayUpdated, ERC7579ExecutorExpirationUpdated.