Skip to main content

Overview

As the smart account ecosystem evolves, Safe periodically releases new versions of the Safe Singleton contract. Each Safe Smart Account is implemented as a SafeProxy, which delegates all logic to a Singleton contract whose address is stored in storage slot 0. Existing SafeProxy contracts can be upgraded to point to a newer Singleton implementation. This process preserves the Safe’s address, owners, and configuration, but requires an explicit migration transaction approved by the Safe owners. The migration is performed via delegatecall and must be executed with care. This guide walks through a step-by-step example of migrating an existing SafeProxy to a newer Singleton using the SafeMigration contract and the Safe Protocol Kit.
Only migrate to trusted and audited Singleton implementations. A malicious or incompatible implementation can take control of the SafeProxy, resulting in permanent loss of access and funds. Always verify compatibility between the existing SafeProxy and the target Singleton version before migrating.

Migration process

The first step in any migration is identifying the address of the target Singleton contract. Safe provides the SafeMigration contract, which updates the Singleton address stored in a SafeProxy.

SafeMigration contract methods

The currently available SafeMigration contract supports upgrades to Safe Singleton v1.4.1.

migrateSingleton()

Updates the SafeProxy to point to the new L1 Singleton implementation.

migrateWithFallbackHandler()

Updates both the Singleton implementation and the fallback handler.

migrateL2Singleton()

Updates the SafeProxy to point to the L2 Singleton implementation.

migrateL2WithFallbackHandler()

Updates the L2 Singleton implementation and the fallback handler.

Requirements

  • An already deployed SafeProxy contract
  • Compatibility between the existing SafeProxy and Singleton v1.4.1
  • For simplicity, this example assumes a Safe with a threshold of 1

Migration tutorial

1

Setup a new project

mkdir safe-migration-tutorial && cd safe-migration-tutorial
npm init -y
npm install @safe-global/protocol-kit @safe-global/types-kit viem
Add typescript support to the project:
npm install --save-dev typescript ts-node
npx tsc --init
2

Add script commands in package.json

The SafeMigration contract provides four methods for migration. Update the package.json to add the following script commands: The migration script will read the argument and choose the appropriate method to execute.
...
  "scripts": {
    ...
    "migrate:L1": "ts-node ./src/migrate.ts migrateSingleton",
    "migrate:L2": "ts-node ./src/migrate.ts migrateL2Singleton",
    "migrate:L1:withFH": "ts-node ./src/migrate.ts migrateWithFallbackHandler",
    "migrate:L2:withFH": "ts-node ./src/migrate.ts migrateL2WithFallbackHandler"
  },
...
3

Create a migration script

Create a new file src/migrate.ts and add the following code:
mkdir src
touch src/migrate.ts
import Safe from "@safe-global/protocol-kit";
import { MetaTransactionData, OperationType } from "@safe-global/types-kit";
import { parseAbi, encodeFunctionData, http, createPublicClient } from "viem";

type MigrationMethod =
  | "migrateSingleton"
  | "migrateWithFallbackHandler"
  | "migrateL2Singleton"
  | "migrateL2WithFallbackHandler";

async function main(migrationMethod: MigrationMethod) {
    // Define constants
    // Build calldata for the migration
    // Initialize the Protocol Kit
    // Create and execute transaction
}

const migrationMethod = process.argv.slice(2)[0] as MigrationMethod;
main(migrationMethod).catch(console.error);
4

Define variables

Define the constants required for the migration script. Replace the placeholders with the actual values.
  // Define constants
  const SAFE_ADDRESS = // ...
  const OWNER_PRIVATE_KEY = // ...
  const RPC_URL = // ...
  const SAFE_MIGRATION_CONTRACT_ADDRESS = // ...
  const ABI = parseAbi([
    "function migrateSingleton() public",
    "function migrateWithFallbackHandler() external",
    "function migrateL2Singleton() public",
    "function migrateL2WithFallbackHandler() external",
  ]);
5

Build `calldata` for the migration

  // Build calldata for the migration
  const calldata = encodeFunctionData({
    abi: ABI,
    functionName: migrationMethod,
  });

  const safeTransactionData: MetaTransactionData = {
    to: SAFE_MIGRATION_CONTRACT_ADDRESS,
    value: "0",
    data: calldata,
    operation: OperationType.DelegateCall,
  };
6

Initialize the Protocol Kit

  // Initialize the Protocol Kit
  const preExistingSafe = await Safe.init({
    provider: RPC_URL,
    signer: OWNER_PRIVATE_KEY,
    safeAddress: SAFE_ADDRESS,
  });
7

Create and execute transaction

  // Create and execute transaction
  const safeTransaction = await preExistingSafe.createTransaction({
    transactions: [safeTransactionData],
  });

  console.log(
    `Executing migration method [${migrationMethod}] using Safe [${SAFE_ADDRESS}]`
  );

  const result = await preExistingSafe.executeTransaction(safeTransaction);

  const publicClient = createPublicClient({
    transport: http(RPC_URL),
  });

  console.log(`Transaction hash [${result.hash}]`);

  await publicClient.waitForTransactionReceipt({
    hash: result.hash as `0x${string}`,
  });
8

Final script

import Safe from "@safe-global/protocol-kit";
import { MetaTransactionData, OperationType } from "@safe-global/types-kit";
import { parseAbi, encodeFunctionData, http, createPublicClient } from "viem";

type MigrationMethod =
  | "migrateSingleton"
  | "migrateWithFallbackHandler"
  | "migrateL2Singleton"
  | "migrateL2WithFallbackHandler";

async function main(migrationMethod: MigrationMethod) {
  const SAFE_ADDRESS = // ...
  const OWNER_PRIVATE_KEY = // ...
  const RPC_URL = // ...
  const SAFE_MIGRATION_CONTRACT_ADDRESS = // ...
  const ABI = parseAbi([
    "function migrateSingleton() public",
    "function migrateWithFallbackHandler() external",
    "function migrateL2Singleton() public",
    "function migrateL2WithFallbackHandler() external",
  ]);

  const calldata = encodeFunctionData({
    abi: ABI,
    functionName: migrationMethod,
  });

  const safeTransactionData: MetaTransactionData = {
    to: SAFE_MIGRATION_CONTRACT_ADDRESS,
    value: "0",
    data: calldata,
    operation: OperationType.DelegateCall,
  };

  const preExistingSafe = await Safe.init({
    provider: RPC_URL,
    signer: OWNER_PRIVATE_KEY,
    safeAddress: SAFE_ADDRESS,
  });

  const safeTransaction = await preExistingSafe.createTransaction({
    transactions: [safeTransactionData],
  });

  console.log(
    `Executing migration method [${migrationMethod}] using Safe [${SAFE_ADDRESS}]`
  );

  const result = await preExistingSafe.executeTransaction(safeTransaction);

  const publicClient = createPublicClient({
    transport: http(RPC_URL),
  });

  console.log(`Transaction hash [${result.hash}]`);
  await publicClient.waitForTransactionReceipt({
    hash: result.hash as `0x${string}`,
  });
}

const migrationMethod = process.argv.slice(2)[0] as MigrationMethod;
main(migrationMethod).catch(console.error);

9

Run the migration script

Run one of the below commands:
npm run migrate:L1
npm run migrate:L2
npm run migrate:L1:withFH
npm run migrate:L2:withFH

Further actions

  • The migration script can be extended to support Safe Account migration with a threshold of more than one. Users can use the Safe API Kit to propose the transactions, fetch transaction data, and sign them.
  • The source code for this script is available in the Safe Migration Script repository.