ModuleSDK
Using Modules
Deadman Switch

How to secure and recovery an account using the Deadman Switch Module

The Deadman Switch Module allows users to specify an inactivity period and a nominee. If the period of time expires without the account owner doing a transaction, the nominee will be able to take over the account. This guide will show you how to install and use the Deadman Switch 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 Deadman Switch Module, and then take over the account with the guardian.

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 { createPublicClient, Hex, http } from 'viem'
import { getAccountNonce } from 'permissionless/actions'
import { createSmartAccountClient } from 'permissionless'
import { toSafeSmartAccount } from 'permissionless/accounts'
import { erc7579Actions } from 'permissionless/actions/erc7579'
import { createPimlicoClient } from 'permissionless/clients/pimlico'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
import {
  createPaymasterClient,
  entryPoint07Address,
  getUserOperationHash,
} from 'viem/account-abstraction'
import {
  RHINESTONE_ATTESTER_ADDRESS,
  MOCK_ATTESTER_ADDRESS,
  getDeadmanSwitch,
  getAccount,
  getClient,
  getDeadmanSwitchValidatorMockSignature,
  getTrustAttestersAction,
  encodeModuleInstallationData,
  encodeValidatorNonce,
} 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 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: '0x3Fdb5BC686e861480ef99A6E3FaAe03c0b9F32e2', // These are not meant to be used in production as of now.
  erc7579LaunchpadAddress: '0xEBe001b3D534B9B6E2500FB78E67a1A137f561CE',
  attesters: [
    RHINESTONE_ATTESTER_ADDRESS, // Rhinestone Attester
    MOCK_ATTESTER_ADDRESS, // Mock Attester - do not use in production
  ],
  attestersThreshold: 1,
})

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 Deadman Switch Module

Next, we will install the Deadman Switch Module on the Safe account. This requires creating a nominee. Then, we will need to install the module as both a validator and a hook. The second time this installation happens, we do not need to pass the initialization data again, but will instead pass empty data. However, we still need to encode this empty data so that it can be correctly interpreted by the account.

const nominee = privateKeyToAccount(
  '0xc171c45f3d35fad832c53cade38e8d21b8d5cc93d1887e867fac626c1c0d6be7',
)
 
const account = getAccount({
  address: safeAccount.address,
  type: 'safe',
})
 
const client = getClient({
  rpcUrl,
})
 
const deadmanSwitch = await getDeadmanSwitch({
  account,
  client,
  nominee: nominee.address,
  timeout: 1,
  moduleType: 'validator',
})
 
const opHash1 = await smartAccountClient.installModule(deadmanSwitch)
 
await pimlicoClient.waitForUserOperationReceipt({
  hash: opHash1,
})
 
const opHash2 = await smartAccountClient.installModule({
  type: 'hook',
  address: deadmanSwitch.module,
  context: encodeModuleInstallationData({
    account,
    module: {
      ...deadmanSwitch,
      initData: '0x',
      type: 'hook',
    },
  }),
})
 
await pimlicoClient.waitForUserOperationReceipt({
  hash: opHash2,
})

Wait for the timeout to expire

Since we set our timeout to 1 second, we can wait for the timeout to expire. In a production environment, setting a low timeout will mean that it will be easier for a hostile nominee to take over the account.

await new Promise((resolve) => setTimeout(resolve, 10000))

Create the takeover UserOperation

Now, we will create a UserOperation from the nominee. The calldata, in this case to the Module Registry is entirely random and a nominee will be able to do any action.

const nonce = await getAccountNonce(publicClient, {
  address: safeAccount.address,
  entryPointAddress: entryPoint07Address,
  key: encodeValidatorNonce({ account, validator: deadmanSwitch }),
})
 
const trustAttestersAction = getTrustAttestersAction({
  threshold: 1,
  attesters: [
    RHINESTONE_ATTESTER_ADDRESS, // Rhinestone Attester
  ],
})
 
const userOperation = await smartAccountClient.prepareUserOperation({
  account: safeAccount,
  calls: [trustAttestersAction],
  nonce: nonce,
  signature: getDeadmanSwitchValidatorMockSignature() as Hex,
})

Sign the taekover UserOperation

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

const userOpHashToSign = getUserOperationHash({
  chainId: chain.id,
  entryPointAddress: entryPoint07Address,
  entryPointVersion: '0.7',
  userOperation,
})
 
userOperation.signature = await nominee.signMessage({
  message: { raw: userOpHashToSign },
})

Execute the takeover UserOperation

Finally, we can execute the UserOperation to take over the account.

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