Safe Smart Account Migration
As the Smart Account ecosystem and standards evolve, Safe Team releases new versions of the Singleton contract as and when required.
The SafeProxy contract (aka. Safe Smart Account) stores the address of the Singleton contract at storage slot(0)
. The existing SafeProxy contracts can be migrated to the new Singleton contract, but the Safe Smart Account owners must take special steps.
The guide below provides a step-by-step process for migrating the SafeProxy contract to the new Singleton contract using delegatecall
.
Only migrate to the trusted and audited contracts. A malicious implementation contract can take over the SafeProxy contract, causing loss of access to the account and potentially losing all funds. Also, verify the compatibility of the new Singleton contract with the existing SafeProxy contract.
Migration Process
Finding the address of the new Singleton contract is the first step in the migration process. Safe maintains SafeMigration (opens in a new tab) contract to update the Singleton contract address in the SafeProxy
contract.
The officially deployed SafeMigration
contract addresses can be found in the Safe Deployments repository (opens in a new tab).
SafeMigration contract methods
Currently available SafeMigration (opens in a new tab) contract supports upgrading to the Safe Singleton contract version 1.4.1.
The contract provides the below functions to migrate the SafeProxy contract to the new Singleton contract.
migrateSingleton()
This function updates the Safe's Singleton address to the new Singleton implementation.
migrateWithFallbackHandler()
This function updates the Safe's Singleton address and the fallback handler to the new implementations.
migrateL2Singleton()
This function updates the Safe's Singleton to the Singleton L2 address.
migrateL2WithFallbackHandler()
This function updates the Safe's Singleton address (to Singleton L2) and the fallback handler to the new implementations.
The tutorial below provides a step-by-step guide for migrating an existing SafeProxy contract to the Singleton v1.4.1 using the Safe Protocol Kit.
Requirements
- A deployed SafeProxy contract.
- The SafeProxy contract should be compatible with the Singleton contract v1.4.1.
- This example assumes that the threshold of the Safe Smart Account is one.
Setup a new project
_10mkdir safe-migration-tutorial && cd safe-migration-tutorial_10npm init -y_10npm install @safe-global/protocol-kit @safe-global/types-kit viem
Add typescript support to the project:
_10npm install --save-dev typescript ts-node_10npx tsc --init
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.
_10..._10 "scripts": {_10 ..._10 "migrate:L1": "ts-node ./src/migrate.ts migrateSingleton",_10 "migrate:L2": "ts-node ./src/migrate.ts migrateL2Singleton",_10 "migrate:L1:withFH": "ts-node ./src/migrate.ts migrateWithFallbackHandler",_10 "migrate:L2:withFH": "ts-node ./src/migrate.ts migrateL2WithFallbackHandler"_10 },_10...
Create a migration script
Create a new file src/migrate.ts
and add the following code:
_10mkdir src_10touch src/migrate.ts
_19import Safe from "@safe-global/protocol-kit";_19import { MetaTransactionData, OperationType } from "@safe-global/types-kit";_19import { parseAbi, encodeFunctionData, http, createPublicClient } from "viem";_19_19type MigrationMethod =_19 | "migrateSingleton"_19 | "migrateWithFallbackHandler"_19 | "migrateL2Singleton"_19 | "migrateL2WithFallbackHandler";_19_19async function main(migrationMethod: MigrationMethod) {_19 // Define constants_19 // Build calldata for the migration_19 // Initialize the Protocol Kit_19 // Create and execute transaction_19}_19_19const migrationMethod = process.argv.slice(2)[0] as MigrationMethod;_19main(migrationMethod).catch(console.error);
Define variables
Define the constants required for the migration script. Replace the placeholders with the actual values.
_11 // Define constants_11 const SAFE_ADDRESS = // ..._11 const OWNER_PRIVATE_KEY = // ..._11 const RPC_URL = // ..._11 const SAFE_MIGRATION_CONTRACT_ADDRESS = // ..._11 const ABI = parseAbi([_11 "function migrateSingleton() public",_11 "function migrateWithFallbackHandler() external",_11 "function migrateL2Singleton() public",_11 "function migrateL2WithFallbackHandler() external",_11 ]);
Build calldata
for the migration
_12 // Build calldata for the migration_12 const calldata = encodeFunctionData({_12 abi: ABI,_12 functionName: migrationMethod,_12 });_12_12 const safeTransactionData: MetaTransactionData = {_12 to: SAFE_MIGRATION_CONTRACT_ADDRESS,_12 value: "0",_12 data: calldata,_12 operation: OperationType.DelegateCall,_12 };
Initialize the Protocol Kit
_10 // Initialize the Protocol Kit_10 const preExistingSafe = await Safe.init({_10 provider: RPC_URL,_10 signer: OWNER_PRIVATE_KEY,_10 safeAddress: SAFE_ADDRESS,_10 });
Create and execute transaction
_20 // Create and execute transaction_20 const safeTransaction = await preExistingSafe.createTransaction({_20 transactions: [safeTransactionData],_20 });_20_20 console.log(_20 `Executing migration method [${migrationMethod}] using Safe [${SAFE_ADDRESS}]`_20 );_20_20 const result = await preExistingSafe.executeTransaction(safeTransaction);_20_20 const publicClient = createPublicClient({_20 transport: http(RPC_URL),_20 });_20_20 console.log(`Transaction hash [${result.hash}]`);_20_20 await publicClient.waitForTransactionReceipt({_20 hash: result.hash as `0x${string}`,_20 });
Final script
_62import Safe from "@safe-global/protocol-kit";_62import { MetaTransactionData, OperationType } from "@safe-global/types-kit";_62import { parseAbi, encodeFunctionData, http, createPublicClient } from "viem";_62_62type MigrationMethod =_62 | "migrateSingleton"_62 | "migrateWithFallbackHandler"_62 | "migrateL2Singleton"_62 | "migrateL2WithFallbackHandler";_62_62async function main(migrationMethod: MigrationMethod) {_62 const SAFE_ADDRESS = // ..._62 const OWNER_PRIVATE_KEY = // ..._62 const RPC_URL = // ..._62 const SAFE_MIGRATION_CONTRACT_ADDRESS = // ..._62 const ABI = parseAbi([_62 "function migrateSingleton() public",_62 "function migrateWithFallbackHandler() external",_62 "function migrateL2Singleton() public",_62 "function migrateL2WithFallbackHandler() external",_62 ]);_62_62 const calldata = encodeFunctionData({_62 abi: ABI,_62 functionName: migrationMethod,_62 });_62_62 const safeTransactionData: MetaTransactionData = {_62 to: SAFE_MIGRATION_CONTRACT_ADDRESS,_62 value: "0",_62 data: calldata,_62 operation: OperationType.DelegateCall,_62 };_62_62 const preExistingSafe = await Safe.init({_62 provider: RPC_URL,_62 signer: OWNER_PRIVATE_KEY,_62 safeAddress: SAFE_ADDRESS,_62 });_62_62 const safeTransaction = await preExistingSafe.createTransaction({_62 transactions: [safeTransactionData],_62 });_62_62 console.log(_62 `Executing migration method [${migrationMethod}] using Safe [${SAFE_ADDRESS}]`_62 );_62_62 const result = await preExistingSafe.executeTransaction(safeTransaction);_62_62 const publicClient = createPublicClient({_62 transport: http(RPC_URL),_62 });_62_62 console.log(`Transaction hash [${result.hash}]`);_62 await publicClient.waitForTransactionReceipt({_62 hash: result.hash as `0x${string}`,_62 });_62}_62_62const migrationMethod = process.argv.slice(2)[0] as MigrationMethod;_62main(migrationMethod).catch(console.error);
Run the migration script
Run one of the below commands:
_10npm run migrate:L1
_10npm run migrate:L2
_10npm run migrate:L1:withFH
_10npm 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 (opens in a new tab) to propose the transactions, fetch transaction data, and sign them.
- The source code for this script is available in the Safe Migration Script repository (opens in a new tab).