How to manage ownership of a Modular Account

The Multi Owner plugin lets your smart accounts have one or more ECDSA or SCA owners. This lets your account integrate with multiple signers at once, and supports recovering your account if one signer is lost.

The Multi-Owner Plugin is able to:

  • Update (add or remove) owners for an MSCA.
  • Check if an address is an owner address of an MSCA.
  • Show all owners of an MSCA.
  • Validate signed signatures of ERC-4337 enabled user operations as well as regular transactions.

All Modular Accounts have MultiOwnerPlugin pre-installed upon creation, exposing following methods for account owners to update (add or remove) and read the current owners of the account:

1/// @notice Update owners of the account. Owners can update owners.
2/// @param ownersToAdd The address array of owners to be added.
3/// @param ownersToRemove The address array of owners to be removed.
4function updateOwners(address[] memory ownersToAdd, address[] memory ownersToRemove) external;
5
6/// @notice Get the owners of `account`.
7/// @param account The account to get the owners of.
8/// @return The addresses of the owners of the account.
9function ownersOf(address account) external view returns (address[] memory);

When you connect your Modular Account to SmartAccountClient you can extend the client with multiOwnerPluginActions, which exposes a set of methods available to call the installed MultiOwnerPlugin with the client connected to the account.

1. Check if an address is one of the current owners of a Modular Account

You should first extend the SmartAccountClient connected to a Modular Account, which already comes with MultiOwnerPlugin installed upon creation, with client to multiOwnerPluginActions for the client to include the MultiOwnerPlugin actions.

When using createModularAccountAlchemyClient in @account-kit/smart-contracts, the SmartAccountClient comes automatically extended with multiOwnerPluginActions, pluginManagerActions, and accountLoupeActions decorators as defaults available for use.

Then, you can use the readOwners method of the multiOwnerPluginActions extended smart account client to check if a given address is one of the current owners of a Modular Account.

import { 
import modularAccountClient
modularAccountClient
} from "./client";
const
const ownerToCheck: "0x..."
ownerToCheck
= "0x..."; // the address of the account to check the ownership of
// returns a boolean whether an address is an owner of account or not const
const isOwner: any
isOwner
= await
import modularAccountClient
modularAccountClient
.
any
isOwnerOf
({
address: string
address
:
const ownerToCheck: "0x..."
ownerToCheck
,
});
import { 
class LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>

Represents a local account signer and provides methods to sign messages and transactions, as well as static methods to create the signer from mnemonic or private key.

LocalAccountSigner
} from "@aa-sdk/core";
import {
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
,
const sepolia: Chain
sepolia
} from "@account-kit/infra";
import {
function createModularAccountAlchemyClient<TSigner extends SmartAccountSigner = SmartAccountSigner<any>>(params: AlchemyModularAccountClientConfig<TSigner>): Promise<AlchemySmartAccountClient<Chain | undefined, MultiOwnerModularAccount<TSigner>, MultiOwnerPluginActions<MultiOwnerModularAccount<TSigner>> & PluginManagerActions<MultiOwnerModularAccount<TSigner>> & AccountLoupeActions<MultiOwnerModularAccount<TSigner>>>>
createModularAccountAlchemyClient
} from "@account-kit/smart-contracts";
import {
function generatePrivateKey(): Hex
generatePrivateKey
} from "viem/accounts";
export const
const chain: Chain
chain
=
const sepolia: Chain
sepolia
;
export const
const modularAccountClient: { account: MultiOwnerModularAccount<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>>; ... 100 more ...; extend: <const client extends { ...; } & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>; }
modularAccountClient
= await
createModularAccountAlchemyClient<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>>(params: AlchemyModularAccountClientConfig<...>): Promise<...>
createModularAccountAlchemyClient
({
signer: LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>
signer
:
class LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>

Represents a local account signer and provides methods to sign messages and transactions, as well as static methods to create the signer from mnemonic or private key.

LocalAccountSigner
.
LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>.privateKeyToAccountSigner(key: Hex): LocalAccountSigner<PrivateKeyAccount>

Creates a LocalAccountSigner instance using the provided private key.

privateKeyToAccountSigner
(
function generatePrivateKey(): Hex
generatePrivateKey
()),
chain: { blockExplorers?: { [key: string]: ChainBlockExplorer; default: ChainBlockExplorer; } | undefined; ... 7 more ...; testnet?: boolean | undefined; } & ChainConfig<...>

Chain for the client.

chain
,
transport: AlchemyTransport

The RPC transport

transport
:
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
({
apiKey: string
apiKey
: "YOUR_API_KEY" }),
});

2. Get all current owners of a Modular Account

You can use the readOwners method on the multiOwnerPluginActions extended smart account client to fetch all current owners of the connected Modular Account.

import { 
import modularAccountClient
modularAccountClient
} from "./client";
// owners is an array of the addresses of the account owners const
const owners: any
owners
= await
import modularAccountClient
modularAccountClient
.
any
readOwners
();
import { 
class LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>

Represents a local account signer and provides methods to sign messages and transactions, as well as static methods to create the signer from mnemonic or private key.

LocalAccountSigner
} from "@aa-sdk/core";
import {
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
,
const sepolia: Chain
sepolia
} from "@account-kit/infra";
import {
function createModularAccountAlchemyClient<TSigner extends SmartAccountSigner = SmartAccountSigner<any>>(params: AlchemyModularAccountClientConfig<TSigner>): Promise<AlchemySmartAccountClient<Chain | undefined, MultiOwnerModularAccount<TSigner>, MultiOwnerPluginActions<MultiOwnerModularAccount<TSigner>> & PluginManagerActions<MultiOwnerModularAccount<TSigner>> & AccountLoupeActions<MultiOwnerModularAccount<TSigner>>>>
createModularAccountAlchemyClient
} from "@account-kit/smart-contracts";
import {
function generatePrivateKey(): Hex
generatePrivateKey
} from "viem/accounts";
export const
const chain: Chain
chain
=
const sepolia: Chain
sepolia
;
export const
const modularAccountClient: { account: MultiOwnerModularAccount<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>>; ... 100 more ...; extend: <const client extends { ...; } & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>; }
modularAccountClient
= await
createModularAccountAlchemyClient<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>>(params: AlchemyModularAccountClientConfig<...>): Promise<...>
createModularAccountAlchemyClient
({
signer: LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>
signer
:
class LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>

Represents a local account signer and provides methods to sign messages and transactions, as well as static methods to create the signer from mnemonic or private key.

LocalAccountSigner
.
LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>.privateKeyToAccountSigner(key: Hex): LocalAccountSigner<PrivateKeyAccount>

Creates a LocalAccountSigner instance using the provided private key.

privateKeyToAccountSigner
(
function generatePrivateKey(): Hex
generatePrivateKey
()),
chain: { blockExplorers?: { [key: string]: ChainBlockExplorer; default: ChainBlockExplorer; } | undefined; ... 7 more ...; testnet?: boolean | undefined; } & ChainConfig<...>

Chain for the client.

chain
,
transport: AlchemyTransport

The RPC transport

transport
:
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
({
apiKey: string
apiKey
: "YOUR_API_KEY" }),
});

3. Add or remove owners for a Modular Account

You can use the updateOwners method on the multiOwnerPluginActions extended smart account client to add or remove owners from the Modular Account.

import { 
import modularAccountClient
modularAccountClient
} from "./client";
const
const ownersToAdd: any[]
ownersToAdd
= []; // the addresses of owners to be added
const
const ownersToRemove: any[]
ownersToRemove
= []; // the addresses of owners to be removed
const
const result: any
result
= await
import modularAccountClient
modularAccountClient
.
any
updateOwners
({
args: any[][]
args
: [
const ownersToAdd: any[]
ownersToAdd
,
const ownersToRemove: any[]
ownersToRemove
],
}); const
const txHash: any
txHash
=
await
import modularAccountClient
modularAccountClient
.
any
waitForUserOperationTransaction
(
const result: any
result
);
import { 
class LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>

Represents a local account signer and provides methods to sign messages and transactions, as well as static methods to create the signer from mnemonic or private key.

LocalAccountSigner
} from "@aa-sdk/core";
import {
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
,
const sepolia: Chain
sepolia
} from "@account-kit/infra";
import {
function createModularAccountAlchemyClient<TSigner extends SmartAccountSigner = SmartAccountSigner<any>>(params: AlchemyModularAccountClientConfig<TSigner>): Promise<AlchemySmartAccountClient<Chain | undefined, MultiOwnerModularAccount<TSigner>, MultiOwnerPluginActions<MultiOwnerModularAccount<TSigner>> & PluginManagerActions<MultiOwnerModularAccount<TSigner>> & AccountLoupeActions<MultiOwnerModularAccount<TSigner>>>>
createModularAccountAlchemyClient
} from "@account-kit/smart-contracts";
import {
function generatePrivateKey(): Hex
generatePrivateKey
} from "viem/accounts";
export const
const chain: Chain
chain
=
const sepolia: Chain
sepolia
;
export const
const modularAccountClient: { account: MultiOwnerModularAccount<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>>; ... 100 more ...; extend: <const client extends { ...; } & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>; }
modularAccountClient
= await
createModularAccountAlchemyClient<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>>(params: AlchemyModularAccountClientConfig<...>): Promise<...>
createModularAccountAlchemyClient
({
signer: LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>
signer
:
class LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>

Represents a local account signer and provides methods to sign messages and transactions, as well as static methods to create the signer from mnemonic or private key.

LocalAccountSigner
.
LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>.privateKeyToAccountSigner(key: Hex): LocalAccountSigner<PrivateKeyAccount>

Creates a LocalAccountSigner instance using the provided private key.

privateKeyToAccountSigner
(
function generatePrivateKey(): Hex
generatePrivateKey
()),
chain: { blockExplorers?: { [key: string]: ChainBlockExplorer; default: ChainBlockExplorer; } | undefined; ... 7 more ...; testnet?: boolean | undefined; } & ChainConfig<...>

Chain for the client.

chain
,
transport: AlchemyTransport

The RPC transport

transport
:
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
({
apiKey: string
apiKey
: "YOUR_API_KEY" }),
});