ModuleSDK
Using Modules
Social Recovery

How to recover an account using the Social Recovery Module

The Social Recovery Module allows users to add a set of guardians to their account so that they can recover it in case they lose access to it. This guide will show you how to install and use the Social Recovery Module on a Safe smart account using the permissionless.js SDK. You can also check out the entire code (opens in a new tab) of the guide.

We will first set up the smart account, install the Social Recovery Module, and then recover the account using the guardians.

Install the packages

First, install the required packages. We use the latest version of module sdk, permissionless ^0.2 and viem ^2.21.

npm i viem @rhinestone/module-sdk permissionless

Import the required functions and constants

import { getAccountNonce } from 'permissionless/actions'
import { createSmartAccountClient } from 'permissionless'
import { toSafeSmartAccount } from 'permissionless/accounts'
import { erc7579Actions } from 'permissionless/actions/erc7579'
import { createPublicClient, http, encodePacked, pad } from 'viem'
import { createPimlicoClient } from 'permissionless/clients/pimlico'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
import {
  createPaymasterClient,
  entryPoint07Address,
  getUserOperationHash,
} from 'viem/account-abstraction'
import {
  getSetOwnableValidatorThresholdAction,
  getSocialRecoveryValidator,
  getOwnableValidator,
  getSocialRecoveryMockSignature,
  RHINESTONE_ATTESTER_ADDRESS,
  MOCK_ATTESTER_ADDRESS,
  encodeValidatorNonce,
  getAccount,
} from '@rhinestone/module-sdk'

Create the clients

Create the smart account client, the bundler client and the paymaster client. You will need to add your own urls here.

const publicClient = createPublicClient({
  transport: http(rpcUrl),
  chain: chain,
})
 
const pimlicoClient = createPimlicoClient({
  transport: http(bundlerUrl),
  entryPoint: {
    address: entryPoint07Address,
    version: '0.7',
  },
})
 
const paymasterClient = createPaymasterClient({
  transport: http(paymasterUrl),
})

Create the signer

The Safe account will need to have a signer to sign user operations. In permissionless.js, the default Safe account validates ECDSA signatures.

For example, to create a signer based on a private key:

const owner = privateKeyToAccount(generatePrivateKey())

Create the initial validator

We will also create and add an initial validator. We are using the Ownable Validator which can be used to verify UserOperations using one or more EOA owners. In this guide, we are installing the validator to then recover it using social recovery.

const ownableValidator = getOwnableValidator({
  owners: [
    '0x2DC2fb2f4F11DeE1d6a2054ffCBf102D09b62bE2',
    '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
  ],
  threshold: 2,
})

Create the Safe account

Create the Safe account object using the signer. Note that you should only use the MockAttester on testnets.

const safeAccount = await toSafeSmartAccount({
  client: publicClient,
  owners: [owner],
  version: '1.4.1',
  entryPoint: {
    address: entryPoint07Address,
    version: '0.7',
  },
  safe4337ModuleAddress: '0x7579EE8307284F293B1927136486880611F20002', 
  erc7579LaunchpadAddress: '0x7579011aB74c46090561ea277Ba79D510c6C00ff',
  attesters: [
    RHINESTONE_ATTESTER_ADDRESS, // Rhinestone Attester
    MOCK_ATTESTER_ADDRESS, // Mock Attester - do not use in production
  ],
  attestersThreshold: 1,
  validators: [
    {
      address: ownableValidator.address,
      context: ownableValidator.initData,
    },
  ],
})

Create the smart account client

The smart account client is used to interact with the smart account. You will need to add your own bundler url and the chain that you are using.

const smartAccountClient = createSmartAccountClient({
  account: safeAccount,
  chain: chain,
  bundlerTransport: http(bundlerUrl),
  paymaster: paymasterClient,
  userOperation: {
    estimateFeesPerGas: async () => {
      return (await pimlicoClient.getUserOperationGasPrice()).fast
    },
  },
}).extend(erc7579Actions())

Install the Social Recovery Module

Next, we will install the Social Recovery Module on the account. We will pass to it a number of guardians that can recover the account as well as a threshold of guardians required to recover the account.

const guardian1 = privateKeyToAccount(
  '0xc171c45f3d35fad832c53cade38e8d21b8d5cc93d1887e867fac626c1c0d6be7',
) // the key coresponding to the first guardian
 
const guardian2 = privateKeyToAccount(
  '0x1a4c05be22dd9294615087ba1dba4266ae68cdc320d9164dbf3650ec0db60f67',
) // the key coresponding to the second guardian
 
const socialRecovery = getSocialRecoveryValidator({
  threshold: 2,
  guardians: [guardian1.address, guardian2.address],
})
 
const opHash1 = await smartAccountClient.installModule(socialRecovery)
 
await pimlicoClient.waitForUserOperationReceipt({
  hash: opHash1,
})

Get the recovery calldata

Finally, we will recover the account using the guardians. In this case, lets assume that the user has lost access to the first signer but still has access to the second one. In this case, the easiest way to recover is to reduce the threshold on the ownable validator to 1 and then the user can add another signer instead.

const recoveryAction = getSetOwnableValidatorThresholdAction({
  threshold: 1,
})

Create the recovery UserOperation

Now that we have the calldata, we can create the UserOperation to recover the account.

const nonce = await getAccountNonce(publicClient, {
  address: safeAccount.address,
  entryPointAddress: entryPoint07Address,
  key: encodeValidatorNonce({
    account: getAccount({
      address: safeAccount.address,
      type: 'safe',
    }),
    validator: socialRecovery,
  }),
})
 
const userOperation = await smartAccountClient.prepareUserOperation({
  account: safeAccount,
  calls: [recoveryAction],
  nonce: nonce,
  signature: getSocialRecoveryMockSignature({
    threshold: 2,
  }),
})

Sign the recovery UserOperation

Next, the guardians will have to sign the recovery UserOperation.

const userOpHashToSign = getUserOperationHash({
  chainId: chain.id,
  entryPointAddress: entryPoint07Address,
  entryPointVersion: '0.7',
  userOperation,
})
 
const signature1 = await guardian1.signMessage({
  message: { raw: userOpHashToSign },
})
 
const signature2 = await guardian2.signMessage({
  message: { raw: userOpHashToSign },
})
 
userOperation.signature = encodePacked(
  ['bytes', 'bytes'],
  [signature1, signature2],
)

Execute the recovery UserOperation

Finally, we can execute the UserOperation to recover the account.

const userOpHash = await smartAccountClient.sendUserOperation(userOperation)
 
const receipt = await pimlicoClient.waitForUserOperationReceipt({
  hash: userOpHash,
})