Skip to content

Commit

Permalink
feat: update safe7579 tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
kopy-kat committed Jan 6, 2025
1 parent 611cfd1 commit 7a969d8
Showing 1 changed file with 144 additions and 69 deletions.
213 changes: 144 additions & 69 deletions pages/safe7579/tutorial-1.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,60 @@ You will set up the smart account, install a Deadman Switch module and then use

### Install the packages

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

```sh npm2yarn
npm i viem @rhinestone/module-sdk permissionless
```

### Import the required packages
### Import the required functions and constants

```typescript copy
import { sepolia } from 'viem/chains'
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 { signerToSafeSmartAccount } from 'permissionless/accounts'
import { createPublicClient, getContract, http, parseEther } from 'viem'
import { createPimlicoBundlerClient } from 'permissionless/clients/pimlico'
import { createPimlicoClient } from 'permissionless/clients/pimlico'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
import {
createPaymasterClient,
entryPoint07Address,
getUserOperationHash,
} from 'viem/account-abstraction'
import {
ENTRYPOINT_ADDRESS_V07,
createSmartAccountClient,
} from 'permissionless'
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 and the bundler client.
Create the smart account client, the bundler client and the paymaster client. You will need to add your own urls here.

```typescript copy
export const publicClient = createPublicClient({
transport: http('https://rpc.ankr.com/eth_sepolia'),
const publicClient = createPublicClient({
transport: http(rpcUrl),
chain: chain,
})

const pimlicoClient = createPimlicoClient({
transport: http(bundlerUrl),
entryPoint: {
address: entryPoint07Address,
version: '0.7',
},
})

export const pimlicoBundlerClient = createPimlicoBundlerClient({
transport: http('https://api.pimlico.io/v2/sepolia/rpc?apikey=API_KEY'),
entryPoint: ENTRYPOINT_ADDRESS_V07,
const paymasterClient = createPaymasterClient({
transport: http(paymasterUrl),
})
```

Expand All @@ -50,110 +74,161 @@ The Safe account will need to have a signer to sign user operations. In permissi
For example, to create a signer based on a private key:

```typescript copy
import { privateKeyToAccount } from 'viem/accounts'

const signer = privateKeyToAccount('0xPRIVATE_KEY')
const owner = privateKeyToAccount(generatePrivateKey())
```

### Create the Safe account

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

```typescript copy
const safeAccount = await signerToSafeSmartAccount(publicClient, {
entryPoint: ENTRYPOINT_ADDRESS_V07,
signer: signer,
saltNonce: 0n, // optional
safeVersion: '1.4.1',
address: '0x...', // optional, only if you are using an already created account
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,
})
```

### Create the smart account client

The smart account client is used to interact with the smart account.
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.

```typescript copy
const smartAccountClient = createSmartAccountClient({
account: safeAccount,
entryPoint: ENTRYPOINT_ADDRESS_V07,
chain: sepolia,
bundlerTransport: http(
'https://api.pimlico.io/v2/sepolia/rpc?apikey=API_KEY',
),
middleware: {
gasPrice: async () => {
return (await pimlicoBundlerClient.getUserOperationGasPrice()).fast
chain: chain,
bundlerTransport: http(bundlerUrl),
paymaster: paymasterClient,
userOperation: {
estimateFeesPerGas: async () => {
return (await pimlicoClient.getUserOperationGasPrice()).fast
},
},
}).extend(erc7579Actions({ entryPoint: ENTRYPOINT_ADDRESS_V07 }))
}).extend(erc7579Actions())
```

### Create the module object
### Install the Deadman Switch Module

Get the module object for the module that you want to install on the smart account. In this case, we will install the Social Recovery Module. 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.
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.

```typescript copy
import { privateKeyToAccount } from 'viem/accounts'
import { getClient, getAccount, getDeadmanSwitch } from '@rhinestone/module-sdk'
const nominee = privateKeyToAccount(
'0xc171c45f3d35fad832c53cade38e8d21b8d5cc93d1887e867fac626c1c0d6be7',
)

const account = await getAccount({
const account = getAccount({
address: safeAccount.address,
type: 'safe',
})

const client = getClient({ rpcUrl: 'your-rpc-url' })

const nominee = privateKeyToAccount('0xPRIVATE_KEY')
const client = getClient({
rpcUrl,
})

const module = getDeadmanSwitch({
const deadmanSwitch = await getDeadmanSwitch({
account,
client,
nominee: nominee.address,
timeout: '100', // in seconds
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.

```typescript copy
await new Promise((resolve) => setTimeout(resolve, 10000))
```

### Install the module as a validator
### Create the takeover UserOperation

With this module object, we can now install it on the smart account as a validator.
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.

```typescript copy
const opHash = await smartClient.installModule({
type: module.type,
address: module.module,
context: module.data,
const nonce = await getAccountNonce(publicClient, {
address: safeAccount.address,
entryPointAddress: entryPoint07Address,
key: encodeValidatorNonce({ account, validator: deadmanSwitch }),
})

await bundlerClientV07.waitForUserOperationReceipt({
hash: opHash,
timeout: 100000,
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,
})
```

### Install the module as a validator
### Sign the taekover UserOperation

The Deadman Switch module is also a hook, so it also needs to be installed as a hook.
Next, the nominee will have to sign the recovery UserOperation.

```typescript copy
const opHash = await smartClient.installModule({
type: 'hook', // manually set the type to hook
address: module.module,
context: encodeAbiParameters(
parseAbiParameters(
'uint8 hookType, bytes4 selector, bytes memory initData',
),
[0, '0x', '0x'],
), // we leave the initData empty since the data was already passed above and install the hook as a global Safe hook
const userOpHashToSign = getUserOperationHash({
chainId: chain.id,
entryPointAddress: entryPoint07Address,
entryPointVersion: '0.7',
userOperation,
})

await bundlerClientV07.waitForUserOperationReceipt({
hash: opHash,
timeout: 100000,
userOperation.signature = await nominee.signMessage({
message: { raw: userOpHashToSign },
})
```

### Recover the account
### Execute the takeover UserOperation

Every transaction, the latest access will be updated. After the timeout has passed, the nominee will be able to use the account. In our case, the timeout is just 100 seconds, so we can recover the account almost immediately.
Finally, we can execute the UserOperation to take over the account.

```typescript copy
const userOpHash = await smartAccountClient.sendUserOperation(userOperation)

const receipt = await pimlicoClient.waitForUserOperationReceipt({
hash: userOpHash,
})
```

</Steps>

0 comments on commit 7a969d8

Please sign in to comment.