From d30a1e2bc27fae4068194948c7113554a002e330 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 19 Jan 2026 15:57:28 -0600 Subject: [PATCH] WIP: feat: add an EIP-7702 impl of single owner LA --- docs-site | 2 +- packages/smart-accounts/src/index.ts | 4 + .../src/light-account/accounts/account.ts | 107 +++++++++++++++--- .../src/light-account/accounts/base.ts | 43 ++++++- .../light-account/lightAccountStaticImpl.ts | 33 +++++- 5 files changed, 166 insertions(+), 23 deletions(-) diff --git a/docs-site b/docs-site index b7bf341e2c..12cae6f6ea 160000 --- a/docs-site +++ b/docs-site @@ -1 +1 @@ -Subproject commit b7bf341e2c9f4cb2fdb57a4ec6eac790f1f12515 +Subproject commit 12cae6f6eafea2c25bab85ac7d0d0bf622471f02 diff --git a/packages/smart-accounts/src/index.ts b/packages/smart-accounts/src/index.ts index f7f3314fe2..cc56630297 100644 --- a/packages/smart-accounts/src/index.ts +++ b/packages/smart-accounts/src/index.ts @@ -39,8 +39,12 @@ export { lightAccountStaticImplV1_0_2, lightAccountStaticImplV1_1_0, lightAccountStaticImplV2_0_0, + lightAccountStaticImplV2_1_0, + lightAccountStaticImplV2_2_0, + lightAccountStaticImpl7702V2_1_0, multiOwnerLightAccountStaticImplV2_0_0, } from "./light-account/lightAccountStaticImpl.js"; +export type { LightAccountStaticImpl7702V2_1_0 } from "./light-account/lightAccountStaticImpl.js"; // ModularAccountV1 export type * from "./ma-v1/accounts/base.js"; diff --git a/packages/smart-accounts/src/light-account/accounts/account.ts b/packages/smart-accounts/src/light-account/accounts/account.ts index 7e6a977f37..85211720a8 100644 --- a/packages/smart-accounts/src/light-account/accounts/account.ts +++ b/packages/smart-accounts/src/light-account/accounts/account.ts @@ -6,9 +6,11 @@ import { type Hex, type JsonRpcAccount, type LocalAccount, + type PrivateKeyAccount, type Transport, } from "viem"; -import { readContract } from "viem/actions"; +import { getCode, readContract } from "viem/actions"; +import type { ToSmartAccountParameters } from "viem/account-abstraction"; import { LightAccountAbi_v1 } from "../abis/LightAccountAbi_v1.js"; import { LightAccountAbi_v2 } from "../abis/LightAccountAbi_v2.js"; import { LightAccountFactoryAbi_v1 } from "../abis/LightAccountFactoryAbi_v1.js"; @@ -23,10 +25,15 @@ import { LightAccountUnsupported1271Factories, defaultLightAccountVersion, } from "../utils.js"; +import { is7702Delegated } from "../../utils.js"; import { toLightAccountBase, type LightAccountBase } from "./base.js"; import { BaseError, lowerAddress } from "@alchemy/common"; import { getAction } from "viem/utils"; import { LOGGER } from "../../logger.js"; +import { InvalidOwnerError } from "../../errors/InvalidOwnerError.js"; +import { lightAccountStaticImpl7702V2_1_0 } from "../lightAccountStaticImpl.js"; + +type LightAccountMode = "default" | "7702"; export type LightAccount< TLightAccountVersion extends @@ -39,15 +46,24 @@ export type LightAccount< export type ToLightAccountParams< TLightAccountVersion extends LightAccountVersion<"LightAccount"> = LightAccountVersion<"LightAccount">, + TMode extends LightAccountMode | undefined = LightAccountMode | undefined, > = { client: Client; owner: JsonRpcAccount | LocalAccount; - salt?: bigint; accountAddress?: Address; - factory?: Address; - factoryData?: Hex; version?: TLightAccountVersion; -}; + mode?: TMode; +} & (TMode extends "7702" + ? { + salt?: never; + factory?: never; + factoryData?: never; + } + : { + salt?: bigint; + factory?: Address; + factoryData?: Hex; + }); /** * Creates a light account. @@ -58,6 +74,7 @@ export type ToLightAccountParams< export async function toLightAccount< TLightAccountVersion extends LightAccountVersion<"LightAccount"> = LightAccountVersion<"LightAccount">, + TMode extends LightAccountMode = LightAccountMode, >({ client, owner, @@ -66,11 +83,15 @@ export async function toLightAccount< version = defaultLightAccountVersion() as TLightAccountVersion, factory = AccountVersionRegistry.LightAccount[version].factoryAddress, factoryData: factoryData_, -}: ToLightAccountParams): Promise< + mode, +}: ToLightAccountParams): Promise< LightAccount > { + const is7702 = mode === "7702"; + LOGGER.debug("toLightAccount:start", { version, + mode, hasAccountAddress: !!accountAddress_, }); @@ -87,16 +108,42 @@ export async function toLightAccount< const accountAddress = accountAddress_ ?? - predictLightAccountAddress({ - factoryAddress: factory, - salt, - ownerAddress: owner.address, - version, - }); + (is7702 && owner.type === "local" + ? owner.address + : predictLightAccountAddress({ + factoryAddress: factory, + salt, + ownerAddress: owner.address, + version, + })); - LOGGER.debug("toLightAccount:address-resolved", { accountAddress }); + LOGGER.debug("toLightAccount:address-resolved", { accountAddress, is7702 }); const getFactoryArgs = async () => { + if (is7702) { + // For 7702 mode, check if the delegation is already set to the correct address. + // We need to do this check here because viem's default isDeployed only checks + // for any code, not whether it's the correct delegation. + const getCodeAction = getAction(client, getCode, "getCode"); + const code = await getCodeAction({ address: accountAddress }); + const delegationAddress = + lightAccountStaticImpl7702V2_1_0.delegationAddress; + const isCorrectlyDelegated = is7702Delegated(delegationAddress, code); + + if (isCorrectlyDelegated) { + return { + factory: undefined, + factoryData: undefined, + }; + } + + // EIP-7702 uses special factory address for EP 0.8 + return { + factory: "0x7702000000000000000000000000000000000000" as Address, + factoryData: "0x" as Hex, + } as const; + } + const factoryData = factoryData_ ?? encodeFunctionData({ @@ -111,6 +158,39 @@ export async function toLightAccount< }; }; + let authorization: ToSmartAccountParameters["authorization"]; + if (is7702) { + LOGGER.debug("toLightAccount:7702-mode"); + if (owner.type !== "local") { + LOGGER.error("toLightAccount:invalid-owner-type", { + ownerType: owner.type, + }); + throw new InvalidOwnerError( + `Owner of type ${owner.type} is unsupported for 7702 mode.`, + ); + } + if (owner.signAuthorization == null) { + LOGGER.error("toLightAccount:missing-signAuthorization"); + throw new InvalidOwnerError( + "Owner must implement `signAuthorization` to be used with 7702 mode.", + ); + } + + // Check if already correctly delegated - if so, no authorization needed + const getCodeAction = getAction(client, getCode, "getCode"); + const code = await getCodeAction({ address: accountAddress }); + const delegationAddress = + lightAccountStaticImpl7702V2_1_0.delegationAddress; + const isCorrectlyDelegated = is7702Delegated(delegationAddress, code); + + if (!isCorrectlyDelegated) { + authorization = { + account: owner as PrivateKeyAccount, + address: delegationAddress, + }; + } + } + const baseAccount = await toLightAccountBase({ client, owner, @@ -119,6 +199,7 @@ export async function toLightAccount< type: "LightAccount", version, getFactoryArgs, + authorization, }); return { diff --git a/packages/smart-accounts/src/light-account/accounts/base.ts b/packages/smart-accounts/src/light-account/accounts/base.ts index 33d5f4b474..1cde33e462 100644 --- a/packages/smart-accounts/src/light-account/accounts/base.ts +++ b/packages/smart-accounts/src/light-account/accounts/base.ts @@ -20,14 +20,21 @@ import { getUserOperationTypedData, toSmartAccount, type SmartAccountImplementation, + type ToSmartAccountParameters, } from "viem/account-abstraction"; -import { getStorageAt, signMessage, signTypedData } from "viem/actions"; +import { + getCode, + getStorageAt, + signMessage, + signTypedData, +} from "viem/actions"; import type { EntryPointFromAccountRegistry, LightAccountType, LightAccountVersion, } from "../registry.js"; import { EIP1967_PROXY_IMPL_STORAGE_SLOT } from "../utils.js"; +import { is7702Delegated } from "../../utils.js"; import { AccountVersionRegistry, isLightAccountVersion2 } from "../registry.js"; import { encodeCallsLA as encodeCalls, @@ -67,7 +74,7 @@ export type BaseLightAccountImplementation< prepareSignature: (request: SignatureRequest) => Promise; formatSignature: (signature: Hex) => Promise; }, - false + boolean >; export type LightAccountBase< @@ -94,6 +101,7 @@ export type ToLightAccountBaseParams< factory?: Address | undefined; factoryData?: Hex | undefined; }>; + authorization?: ToSmartAccountParameters["authorization"]; }; export async function toLightAccountBase< @@ -109,6 +117,7 @@ export async function toLightAccountBase< type, version, getFactoryArgs, + authorization, }: ToLightAccountBaseParams< TLightAccountType, TLightAccountVersion, @@ -228,10 +237,17 @@ export async function toLightAccountBase< AccountVersionRegistry[type][version] as StaticSmartAccountImplementation ).entryPoint; - return await toSmartAccount({ + // Extract delegation address for 7702 mode + const delegationAddress = + authorization && "address" in authorization + ? authorization.address + : undefined; + + const account = await toSmartAccount({ getFactoryArgs, client, entryPoint, + authorization, async getAddress() { return accountAddress; @@ -376,4 +392,25 @@ export async function toLightAccountBase< formatSignature, }, }); + + // For 7702 mode, we need to override getFactoryArgs because viem's default + // isDeployed check returns true for any code at the address, but for 7702 + // we need to check if it's delegated to the CORRECT address. + if (delegationAddress) { + account.getFactoryArgs = async () => { + const getCodeAction = getAction(client, getCode, "getCode"); + const code = await getCodeAction({ address: accountAddress }); + const isCorrectlyDelegated = is7702Delegated(delegationAddress, code); + + if (isCorrectlyDelegated) { + return { factory: undefined, factoryData: undefined }; + } + + // Not correctly delegated - call our original getFactoryArgs + // which returns the 0x7702 factory + return getFactoryArgs(); + }; + } + + return account; } diff --git a/packages/smart-accounts/src/light-account/lightAccountStaticImpl.ts b/packages/smart-accounts/src/light-account/lightAccountStaticImpl.ts index 4f6332e401..56940b8a33 100644 --- a/packages/smart-accounts/src/light-account/lightAccountStaticImpl.ts +++ b/packages/smart-accounts/src/light-account/lightAccountStaticImpl.ts @@ -174,6 +174,15 @@ export const multiOwnerLightAccountStaticImplV2_0_0: StaticSmartAccountImplement }, }; +const lightAccountBaseV2_1_0 = { + entryPoint: { + abi: entryPoint08Abi, + address: entryPoint08Address, + version: "0.8", + }, + accountAbi: LightAccountAbi_v2, +} as const; + export const lightAccountStaticImplV2_1_0: StaticSmartAccountImplementation< false, "0.8", @@ -182,12 +191,7 @@ export const lightAccountStaticImplV2_1_0: StaticSmartAccountImplementation< typeof LightAccountAbi_v2, typeof LightAccountFactoryAbi_v2 > = { - entryPoint: { - abi: entryPoint08Abi, - address: entryPoint08Address, - version: "0.8", - }, - accountAbi: LightAccountAbi_v2, + ...lightAccountBaseV2_1_0, accountImplementation: lowerAddress( "0x2c53D0bD33A60db8881c7b049Df6fd762A1f059C", ), @@ -209,6 +213,23 @@ export const lightAccountStaticImplV2_1_0: StaticSmartAccountImplementation< }, }; +export type LightAccountStaticImpl7702V2_1_0 = StaticSmartAccountImplementation< + true, + "0.8", + {}, + typeof entryPoint08Abi, + typeof LightAccountAbi_v2, + never +>; + +export const lightAccountStaticImpl7702V2_1_0: LightAccountStaticImpl7702V2_1_0 = + { + ...lightAccountBaseV2_1_0, + delegationAddress: lowerAddress( + "0x82CfFc0f83A66F016f1273CdD5C43f86E78d2478", + ), + }; + export const lightAccountStaticImplV2_2_0: StaticSmartAccountImplementation< false, "0.9",