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,
})