Not all smart account implementations support transferring ownership (e.g. SimpleAccount), but LightAccount does. Below shows how to do it using @alchemy/smart-accounts.
LightAccount exposes the following onchain method which allows the current owner to transfer ownership to a new owner address:
function transferOwnership(address newOwner) public virtual onlyOwnertoLightAccount returns an encodeTransferOwnership helper. Encode the calldata for the target new owner, send it as a user operation from the account, and (optionally) re-connect a fresh account using the new owner so you can keep sending UOs after the transfer.
import { createBundlerClient } from "viem/account-abstraction";
import { createClient } from "viem";
import { sepolia } from "viem/chains";
import { mnemonicToAccount, privateKeyToAccount, generatePrivateKey } from "viem/accounts";
import { alchemyTransport } from "@alchemy/common";
import { toLightAccount } from "@alchemy/smart-accounts";
import { estimateFeesPerGas } from "@alchemy/aa-infra";
const ALCHEMY_API_KEY = "your-api-key";
const transport = alchemyTransport({ apiKey: ALCHEMY_API_KEY });
const rpcClient = createClient({ chain: sepolia, transport });
// Original owner + account.
const originalOwner = privateKeyToAccount(generatePrivateKey());
const account = await toLightAccount({
client: rpcClient,
owner: originalOwner,
version: "v2.0.0",
});
const bundlerClient = createBundlerClient({
account,
client: rpcClient,
chain: sepolia,
transport,
userOperation: { estimateFeesPerGas },
});
// 1) Pick a new owner.
const newOwner = mnemonicToAccount(process.env.NEW_OWNER_MNEMONIC!);
// 2) Encode + send the transfer.
const data = account.encodeTransferOwnership(newOwner.address);
const hash = await bundlerClient.sendUserOperation({
calls: [{ to: account.address, data }],
});
await bundlerClient.waitForUserOperationReceipt({ hash });
// 3) After the transfer mines, reconnect to the same account address with the new owner.
// NOTE: you MUST pass the original accountAddress — otherwise the new owner derives
// a different counterfactual address.
const transferredAccount = await toLightAccount({
client: rpcClient,
owner: newOwner,
version: "v2.0.0",
accountAddress: account.address,
});
const transferredClient = createBundlerClient({
account: transferredAccount,
client: rpcClient,
chain: sepolia,
transport,
userOperation: { estimateFeesPerGas },
});You can also read the current onchain owner via account.getOwnerAddress() to verify the transfer landed.
See the Light Account overview for more details on the contract.