Skip to main content

Safe Account V3

Safe Account V3 uses the original Safe Singleton and adds ERC-4337 functionality using a module/fallback handler.

The V3 contracts, known as the SafeAccountV0_3_0 class in AbstractionKit, supports EntryPoint v0.7.

Import

import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit"; 

How to use

Initialize a new Safe Account and calculate its address:

const ownerPublicAddress = "0xBdbc5FBC9cA8C3F514D073eC3de840Ac84FC6D31"; // Safe owner pub address
const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]);

const accountAddress = smartAccount.accountAddress;

Methods

The Essentials methods provides all necessary functionalities with support for overrides, offering a streamlined approach.

initializeNewAccount

Initilizes a new SafeAccount class given a list of owners of public address(es). Only need to be called on the first transaction when the account has not been deployed yet.

Usage

In this example, we initiate a single owner account.

example.ts
import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";

const ownerPublicAddress = "0xBdbc5FBC9cA8C3F514D073eC3de840Ac84FC6D31";
const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]);

console.log("Account address (sender): " + smartAccount.accountAddress);
Example Response
Account address(sender) : 0x1a02592A3484c2077d2E5D24482497F85e1980C6

Source code

initializeNewAccount

createUserOperation

This method determines the nonce, fetch the gas prices, estimate gas limits and return a useroperation to be signed. You can override any of these values using the overrides parameter.

Usage

This example mints the same NFT twice in a single useroperation

example.ts
import { MetaTransaction } from "abstractionkit";

const jsonRpcNodeProvider = "https://rpc2.sepolia.org";
const bundlerUrl = "https://sepolia.voltaire.candidewallet.com/rpc";

const transaction: MetaTransaction = {
to: "0xD9de104e3386d9A45a61BcE269c43E48B534e4E7", // NFT contract address
value: 0n,
data: "0x1249c58b", // mint()
}

let userOperation = await smartAccount.createUserOperation(
[transaction, transaction], // batch transactions to mint 2 NFTs
jsonRpcNodeProvider,
bundlerUrl,
)

console.log(userOperation);
Example Response
{
sender: '0xd785bb8a95a6a08ace0aa2e54aee5cf04694b1db',
nonce: 19n,
callData: '0x541d63c80000000000000000000000009a7af758ae5d7b6aae84fe4c5ba67c041dfe533600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000246a627842000000000000000000000000d785bb8a95a6a08ace0aa2e54aee5cf04694b1db00000000000000000000000000000000000000000000000000000000',
callGasLimit: 58588n,
verificationGasLimit: 94374n,
preVerificationGas: 45628n,
maxFeePerGas: 148180551200n,
maxPriorityFeePerGas: 161327244n,
signature: '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000041ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
factory: null,
factoryData: null,
paymaster: null,
paymasterVerificationGasLimit: null,
paymasterPostOpGasLimit: null,
paymasterData: null
}

Source code

createUserOperation

signUserOperation

This method takes a userOperation, the private keys of the owner of the account, and the chainId and returns the signature field.

example.ts
const chainId = BigInt("11155111"); // sepolia chain ID
const privateKey = "0x4cad764980d84fc6684ca839cae2c78be5432e292fa98416e11687ceb9096a03";
const userOperation = {..}

const signature = smartAccount.signUserOperation(
userOperation,
[privateKey],
chainId,
);

userOperation.signature = signature;

console.log(signature);
Example Response
0x00000000000000000000000041c6297bd9573e8d979a272db4f6576a98f639a7e6874055a627769401dc46d01143551ccaa473364ace4340ec395c546dccb725e1eac2639ecef443d229f0071b

Source code

signUserOperation

sendUserOperation

This method sends the userop to the bundler to be executed onchain. It returns a promise SendUseroperationResponse object to confirm the on-chain inclusion of the userop

example.ts
const sendUserOperationResponse = await smartAccount.sendUserOperation(userOperation, bundlerUrl)

console.log("sendUserOperationResponse: ". sendUserOperationResponse);
console.log("Useroperation sent. Waiting to be included...");

const receipt = await sendUserOperationResponse.included()

console.log("receipt: ", receipt);
Example Response
sendUserOperationResponse: {
userOperationHash: '0x61b3e2c57ad7ad1ae788f0ac84c79b28aab8aeaf872be173cadc72ab8b3d4418',
bundler: { rpcUrl: 'https://sepolia.voltaire.candidewallet.com/rpc' },
entrypointAddress: '0x0000000071727De22E5E9d8BAf0edAc6f37da032'
}

Useroperation sent. Waiting to be included...

receipt: {
userOpHash: '0x395ddb51c0b76fd72796878d1008c8c9af897e944092bbc741b0a8def1a29984',
entryPoint: '0x0000000071727De22E5E9d8BAf0edAc6f37da032',
sender: '0xd785bb8a95a6a08ace0aa2e54aee5cf04694b1db',
nonce: 19n,
paymaster: '0x8b1f6cb5d062aa2ce8d581942bbb960420d875ba',
actualGasCost: 16706333689468320n,
actualGasUsed: 185340n,
success: true,
logs: '[{"address":"0x0000000071727de22e5e9d8baf0edac6f37da032","topics":["0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f","0x395ddb51c0b76fd72796878d1008c8c9af897e944092bbc741b0a8def1a29984","0x000000000000000000000000d785bb8a95a6a08ace0aa2e54aee5cf04694b1db","0x0000000000000000000000008b1f6cb5d062aa2ce8d581942bbb960420d875ba"],"data":"0x00000000000000000000000000000000000000000000000000000000000000130000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003b5a526d0f55a0000000000000000000000000000000000000000000000000000000000002d3fc","blockNumber":"0x65e483","transactionHash":"0xdf4f976eb8148d14ded74c3f1b21666250be9ae836aa42fb97a3e5ba23f6bf8e","transactionIndex":"0x5d","blockHash":"0x0f306649579021da8b4447bdfcf24b050bacc162ddb756c1d003ab2dd7a0ad9f","logIndex":"0x79","removed":false}]',
receipt: {
blockHash: '0x0f306649579021da8b4447bdfcf24b050bacc162ddb756c1d003ab2dd7a0ad9f',
blockNumber: 6677635n,
from: '0x3cfdc212769c890907bce93d3d8c2c53de6a7a89',
cumulativeGasUsed: 10239342n,
gasUsed: 175887n,
logs: '[{"address":"0x0000000071727de22e5e9d8baf0edac6f37da032","topics":["0xbb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972"],"data":"0x","blockNumber":"0x65e483","transactionHash":"0xdf4f976eb8148d14ded74c3f1b21666250be9ae836aa42fb97a3e5ba23f6bf8e","transactionIndex":"0x5d","blockHash":"0x0f306649579021da8b4447bdfcf24b050bacc162ddb756c1d003ab2dd7a0ad9f","logIndex":"0x74","removed":false},{"address":"0xd785bb8a95a6a08ace0aa2e54aee5cf04694b1db","topics":["0xb648d3644f584ed1c2232d53c46d87e693586486ad0d1175f8656013110b714e"],"data":"0x00000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c2260000000000000000000000009a7af758ae5d7b6aae84fe4c5ba67c041dfe5336000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000246a627842000000000000000000000000d785bb8a95a6a08ace0aa2e54aee5cf04694b1db00000000000000000000000000000000000000000000000000000000","blockNumber":"0x65e483","transactionHash":"0xdf4f976eb8148d14ded74c3f1b21666250be9ae836aa42fb97a3e5ba23f6bf8e","transactionIndex":"0x5d","blockHash":"0x0f306649579021da8b4447bdfcf24b050bacc162ddb756c1d003ab2dd7a0ad9f","logIndex":"0x75","removed":false},{"address":"0x9a7af758ae5d7b6aae84fe4c5ba67c041dfe5336","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000d785bb8a95a6a08ace0aa2e54aee5cf04694b1db","0x0000000000000000000000000000000000000000000000000000000000000508"],"data":"0x","blockNumber":"0x65e483","transactionHash":"0xdf4f976eb8148d14ded74c3f1b21666250be9ae836aa42fb97a3e5ba23f6bf8e","transactionIndex":"0x5d","blockHash":"0x0f306649579021da8b4447bdfcf24b050bacc162ddb756c1d003ab2dd7a0ad9f","logIndex":"0x76","removed":false},{"address":"0xd785bb8a95a6a08ace0aa2e54aee5cf04694b1db","topics":["0x6895c13664aa4f67288b25d7a21d7aaa34916e355fb9b6fae0a139a9085becb8","0x00000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c226"],"data":"0x","blockNumber":"0x65e483","transactionHash":"0xdf4f976eb8148d14ded74c3f1b21666250be9ae836aa42fb97a3e5ba23f6bf8e","transactionIndex":"0x5d","blockHash":"0x0f306649579021da8b4447bdfcf24b050bacc162ddb756c1d003ab2dd7a0ad9f","logIndex":"0x77","removed":false},{"address":"0x8b1f6cb5d062aa2ce8d581942bbb960420d875ba","topics":["0xa050a122b4c0e369e3385eb6b7cccd8019638b2764de67bec0af99130ddf8471","0x395ddb51c0b76fd72796878d1008c8c9af897e944092bbc741b0a8def1a29984","0x000000000000000000000000d785bb8a95a6a08ace0aa2e54aee5cf04694b1db","0x0000000000000000000000000000000000000000000000000000000000000000"],"data":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x65e483","transactionHash":"0xdf4f976eb8148d14ded74c3f1b21666250be9ae836aa42fb97a3e5ba23f6bf8e","transactionIndex":"0x5d","blockHash":"0x0f306649579021da8b4447bdfcf24b050bacc162ddb756c1d003ab2dd7a0ad9f","logIndex":"0x78","removed":false},{"address":"0x0000000071727de22e5e9d8baf0edac6f37da032","topics":["0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f","0x395ddb51c0b76fd72796878d1008c8c9af897e944092bbc741b0a8def1a29984","0x000000000000000000000000d785bb8a95a6a08ace0aa2e54aee5cf04694b1db","0x0000000000000000000000008b1f6cb5d062aa2ce8d581942bbb960420d875ba"],"data":"0x00000000000000000000000000000000000000000000000000000000000000130000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003b5a526d0f55a0000000000000000000000000000000000000000000000000000000000002d3fc","blockNumber":"0x65e483","transactionHash":"0xdf4f976eb8148d14ded74c3f1b21666250be9ae836aa42fb97a3e5ba23f6bf8e","transactionIndex":"0x5d","blockHash":"0x0f306649579021da8b4447bdfcf24b050bacc162ddb756c1d003ab2dd7a0ad9f","logIndex":"0x79","removed":false}]',
logsBloom: '0x0000000000111000000010000000000000200000000000000000000002000000000800040000020000000001000040000000000000000000800002000000000020000000020000000000000c000000000040000010000000000000000000000000000000020800000000000000000800000000000000200000000010000000000000000020100000000000000800000020000000000000008000000004000000000040100000000800400080000000000200000040000000000002000000000000000002000000400001000000000000000000000020000000100000000020000000200000000000000200000000000200000000000000000000000010000000',
transactionHash: '0xdf4f976eb8148d14ded74c3f1b21666250be9ae836aa42fb97a3e5ba23f6bf8e',
transactionIndex: 93n,
effectiveGasPrice: 50000149713n
}
}

Source code

sendUserOperation

Advanced Methods

The Advanced methods offer fine control and customization, catering to developers who require detailed configurations for their specific requirements.

createAccountAddress

Calculates the Account address from the initial owners

Usage

In this example, we initiate a single owner account.

example.ts
import { SafeAccount } from "abstractionkit";

const ownerPublicAddress = "0xBdbc5FBC9cA8C3F514D073eC3de840Ac84FC6D31";
const safeAddress = SafeAccount.createAccountAddress(
[ownerPublicAddress],
);

console.log("Account address (sender): " + safeAddress);
Example Response
Account address(sender) : 0x1a02592A3484c2077d2E5D24482497F85e1980C6

Source code

createAccountAddress

createFactoryAddressAndData

Create an account factory address and factory data

Usage

In this example, we initiate a single owner account.

example.ts
import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";

const ownerPublicAddress = "0xBdbc5FBC9cA8C3F514D073eC3de840Ac84FC6D31";
let [factoryAddress, factoryData] = SafeAccount.createFactoryAddressAndData(
[ownerPublicAddress],
);

console.log("factoryAddress: " + factoryAddress);
console.log("factoryData: ", factoryData);
Example Response
factoryAddress: 0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67
factoryData: 0x1688f0b900000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c7620000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e4b63e800d000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000002dd68b007b46fbe91b9a7c3eda5a7a1063cb5b47000000000000000000000000000000000000000000000000000000000000014000000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c2260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bdbc5fbc9ca8c3f514d073ec3de840ac84fc6d3100000000000000000000000000000000000000000000000000000000000000648d0dc49f0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c2260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

Source code

createFactoryAddressAndData

createInitializerCallData

Creates the initilizer calldata

Usage

import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";

const initializeCallData = SafeAccount.createInitializerCallData(
[ownerPublicAddress], // owners
1, //threshold
);
console.log("initializeCallData: " + initializeCallData);
Example Response
initializeCallData: 0xb63e800d000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000002dd68b007b46fbe91b9a7c3eda5a7a1063cb5b47000000000000000000000000000000000000000000000000000000000000014000000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c2260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bdbc5fbc9ca8c3f514d073ec3de840ac84fc6d3100000000000000000000000000000000000000000000000000000000000000648d0dc49f0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c22600000000000000000000000000000000000000000000000000000000

Source

createInitializerCallData

createAccountCallDataSingleTransaction

Encode calldata for a single MetaTransaction to be executed by Safe account

Usage

In this example, we make a transfer of 1 wei to a random address.

example.ts
import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";

const ownerPublicAddress = "0xBdbc5FBC9cA8C3F514D073eC3de840Ac84FC6D31";
const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]);

const callData = smartAccount.createAccountCallDataSingleTransaction({
to: "0x1a02592A3484c2077d2E5D24482497F85e1980C6",
value: 1,
data: "0x",
});

console.log("callData: " + callData);
Example Response
callData : 0xf34308ef000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

Source code

createAccountCallDataSingleTransaction

createAccountCallDataBatchTransactions

Encode calldata for a list of MetaTransactions to be executed by Safe account

Usage

In this example, we make a transfer to 2 different random addresses, 1 wei each.

example.ts
import {
SafeAccountV0_3_0 as SafeAccount,
MetaTransaction,
} from "abstractionkit";

const ownerPublicAddress = "0xBdbc5FBC9cA8C3F514D073eC3de840Ac84FC6D31";
const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]);

const tx1: MetaTransaction = {
to: "0x1a02592A3484c2077d2E5D24482497F85e1980C6",
value: 1,
data: "0x",
};
const tx2: MetaTransaction = {
to: "0x3fe285dcd76bcce4ac92d38a6f2f8e964041e020",
value: 1,
data: "0x",
};

const callData = SafeAccount.createAccountCallDataBatchTransactions([tx1, tx2]);

console.log("callData: " + callData);
Example Response
callData : 0xf34308ef000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

Source code

createAccountCallDataBatchTransactions

estimateUserOperationGas

Estimate gas limits for a userOperation

Usage

import { 
SafeAccountV0_3_0 as SafeAccount,
UserOperationV7
} from "abstractionkit";

const bundlerRPC = "https://sepolia.voltaire.candidewallet.com/rpc";

const ownerPublicAddress = "0xBdbc5FBC9cA8C3F514D073eC3de840Ac84FC6D31";
const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress])

// Use createUserOperation() to help you construct the userOp below
const userOperation:UserOperationV7 = {..}

const [preVerificationGas, verificationGasLimit, callGasLimit] = await estimateUserOperationGas(userOperation, bundlerRPC);
Example Response
[ 46840n, 64545n, 102761n ]

Source code

estimateUserOperationGas

getUserOperationEip712Hash

Create a userOperation eip712 hash

Usage

example
import { 
SafeAccountV0_3_0 as SafeAccount,
UserOperationV7
} from "abstractionkit";

const userOperation: UserOperationV7 = smartAccount.createUserOperation(..)

const userOpEip712Hash = SafeAccount.getUserOperationEip712Hash(userOperation, chainId);

console.log(userOpEip712Hash);
Example Response
0xec030c825b12b398c10f1b552004e43ec753fdf001e1c1daa1ceffe4f7ff5056

Source

getUserOperationEip712Hash

formatEip712SignaturesToUseroperationSignature

A static method to format a list of eip712 signatures to a userOperation signature.

Usage

import { SafeAccountV0_3_0 as SafeAccount, EIP712_SAFE_OPERATION_V7_TYPE } from "abstractionkit";
import { Wallet } from "ethers";

const ownerPublicAddress = process.env.PUBLIC_ADDRESS as string;
const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]);

let userOperation = ... // Use createUserOperation() to help you construct the userOp below

const domain = {
chainId: process.env.CHAIN_ID,
verifyingContract: smartAccount.safe4337ModuleAddress,
};

const types = EIP712_SAFE_OPERATION_V7_TYPE;

// formate according to EIP712 Safe Operation Type
const { sender, ...userOp } = userOperation;
const safeUserOperation = {
...userOp,
safe: userOperation.sender,
validUntil: BigInt(0),
validAfter: BigInt(0),
entryPoint: smartAccount.entrypointAddress,
};

const ownerPrivateKey = process.env.PRIVATE_KEY as string;
const signer = new Wallet(ownerPrivateKey);
const signature = await signer.signTypedData(domain, types, safeUserOperation);
const formatedSig = SafeAccount.formatEip712SignaturesToUseroperationSignature([ownerPublicAddress], [signature]);
userOperation.signature = formatedSig;
Example Response
0x0000000000000000000000006da39f6f7b0d2c0035084d3c313350697b3167ff591a84bf0b4bb4741224b5d226682ec306544c091e2b6535042c900b459282edfe98e393d552963ca8db11731c

Source code

formatEip712SignaturesToUseroperationSignature

Audits