diff --git a/bun.lockb b/bun.lockb index e377454f..f062ad54 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/core/accounts/kernel/createKernelAccount.ts b/packages/core/accounts/kernel/createKernelAccount.ts index 936c5893..656d4072 100644 --- a/packages/core/accounts/kernel/createKernelAccount.ts +++ b/packages/core/accounts/kernel/createKernelAccount.ts @@ -8,6 +8,7 @@ import { type Chain, type Client, type EncodeDeployDataParameters, + type Hash, type Hex, type Transport, type TypedDataDefinition, @@ -21,16 +22,19 @@ import { hashTypedData, keccak256, parseAbi, + publicActions, stringToHex, validateTypedData } from "viem" import { toAccount } from "viem/accounts" import { getBytecode } from "viem/actions" +import { KERNEL_NAME, LATEST_KERNEL_VERSION } from "../../constants.js" import type { KernelEncodeCallDataArgs, KernelPluginManager, KernelPluginManagerParams } from "../../types/kernel.js" +import { wrapSignatureWith6492 } from "../utils/6492.js" import { isKernelPluginManager, toKernelPluginManager @@ -202,6 +206,14 @@ const getAccountAddress = async < }) } +const parseFactoryAddressAndCallDataFromAccountInitCode = ( + initCode: Hex +): [Address, Hex] => { + const factoryAddress = `0x${initCode.substring(2, 42)}` as Address + const factoryCalldata = `0x${initCode.substring(42)}` as Hex + return [factoryAddress, factoryCalldata] +} + /** * Build a kernel smart account from a private key, that use the ECDSA signer behind the scene * @param client @@ -256,14 +268,13 @@ export async function createKernelAccount< } // Fetch account address and chain id - const [accountAddress] = await Promise.all([ + const accountAddress = deployedAccountAddress ?? - getAccountAddress({ - client, - entryPoint, - initCodeProvider: generateInitCode - }) - ]) + (await getAccountAddress({ + client, + entryPoint, + initCodeProvider: generateInitCode + })) if (!accountAddress) throw new Error("Account address not found") @@ -281,11 +292,28 @@ export async function createKernelAccount< "latest" ] }) - const decoded = decodeFunctionResult({ - abi: [...EIP1271ABI], - functionName: "eip712Domain", - data: domain - }) + + let name: string + let version: string + let chainId: bigint + + if (domain !== "0x") { + const decoded = decodeFunctionResult({ + abi: [...EIP1271ABI], + functionName: "eip712Domain", + data: domain + }) + + name = decoded[1] + version = decoded[2] + chainId = decoded[3] + } else { + name = KERNEL_NAME + version = LATEST_KERNEL_VERSION + chainId = client.chain + ? client.chain.id + : await client.extend(publicActions).getChainId() + } const encoded = encodeAbiParameters( [ @@ -301,9 +329,9 @@ export async function createKernelAccount< "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" ) ), - keccak256(stringToHex(decoded[1])), - keccak256(stringToHex(decoded[2])), - decoded[3], + keccak256(stringToHex(name)), + keccak256(stringToHex(version)), + BigInt(chainId), accountAddress ] ) @@ -324,7 +352,11 @@ export async function createKernelAccount< address: accountAddress, async signMessage({ message }) { const messageHash = hashMessage(message) - return signHashedMessage(messageHash) + const [isDeployed, signature] = await Promise.all([ + isAccountDeployed(), + signHashedMessage(messageHash) + ]) + return create6492Signature(isDeployed, signature) }, async signTransaction(_, __) { throw new SignTransactionNotSupportedBySmartAccount() @@ -347,16 +379,50 @@ export async function createKernelAccount< } as TypedDataDefinition) const typedHash = hashTypedData(typedData) - return await signHashedMessage(typedHash) + const [isDeployed, signature] = await Promise.all([ + isAccountDeployed(), + signHashedMessage(typedHash) + ]) + return create6492Signature(isDeployed, signature) } }) + const isAccountDeployed = async (): Promise => { + const contractCode = await getBytecode(client, { + address: accountAddress + }) + + return (contractCode?.length ?? 0) > 2 + } + + const create6492Signature = async ( + isDeployed: boolean, + signature: Hash + ): Promise => { + if (isDeployed) { + return signature + } + + const [factoryAddress, factoryCalldata] = + parseFactoryAddressAndCallDataFromAccountInitCode( + await generateInitCode() + ) + + return wrapSignatureWith6492({ + factoryAddress, + factoryCalldata, + signature + }) + } + return { ...account, client: client, publicKey: accountAddress, entryPoint: entryPoint, source: "kernelSmartAccount", + kernelPluginManager, + generateInitCode, // Get the nonce of the smart account async getNonce() { @@ -367,23 +433,19 @@ export async function createKernelAccount< key }) }, - kernelPluginManager, // Sign a user operation async signUserOperation(userOperation) { return kernelPluginManager.signUserOperation(userOperation) }, - generateInitCode, // Encode the init code async getInitCode() { - const contractCode = await getBytecode(client, { - address: accountAddress - }) - - if ((contractCode?.length ?? 0) > 2) return "0x" - - return generateInitCode() + if (await isAccountDeployed()) { + return "0x" + } else { + return generateInitCode() + } }, // Encode the deploy call data diff --git a/packages/core/accounts/utils/6492.ts b/packages/core/accounts/utils/6492.ts new file mode 100644 index 00000000..59dde213 --- /dev/null +++ b/packages/core/accounts/utils/6492.ts @@ -0,0 +1,70 @@ +// copied from: https://github.com/alchemyplatform/aa-sdk/blob/266c9757cd721ef0bd97d04c0b592a329f8a9da5/packages/core/src/signer/utils.ts + +import { + type Address, + type Hash, + type Hex, + type PublicClient, + concat, + encodeAbiParameters, + parseAbiParameters +} from "viem" + +export type SignWith6492Params = { + factoryAddress: Address + factoryCalldata: Hex + signature: Hash +} + +type VerifyEIP6492SignatureParams = { + signer: Address + hash: Hash + signature: Hash + client: PublicClient +} + +export const wrapSignatureWith6492 = ({ + factoryAddress, + factoryCalldata, + signature +}: SignWith6492Params): Hash => { + // wrap the signature as follows: https://eips.ethereum.org/EIPS/eip-6492 + // concat( + // abi.encode( + // (create2Factory, factoryCalldata, originalERC1271Signature), + // (address, bytes, bytes)), + // magicBytes + // ) + return concat([ + encodeAbiParameters(parseAbiParameters("address, bytes, bytes"), [ + factoryAddress, + factoryCalldata, + signature + ]), + "0x6492649264926492649264926492649264926492649264926492649264926492" + ]) +} + +// defined in https://github.com/AmbireTech/signature-validator/blob/main/index.ts#L13C17-L13C17 +const universalValidatorByteCode = + "0x60806040523480156200001157600080fd5b50604051620007003803806200070083398101604081905262000034916200056f565b6000620000438484846200004f565b9050806000526001601ff35b600080846001600160a01b0316803b806020016040519081016040528181526000908060200190933c90507f6492649264926492649264926492649264926492649264926492649264926492620000a68462000451565b036200021f57600060608085806020019051810190620000c79190620005ce565b8651929550909350915060000362000192576000836001600160a01b031683604051620000f5919062000643565b6000604051808303816000865af19150503d806000811462000134576040519150601f19603f3d011682016040523d82523d6000602084013e62000139565b606091505b5050905080620001905760405162461bcd60e51b815260206004820152601e60248201527f5369676e617475726556616c696461746f723a206465706c6f796d656e74000060448201526064015b60405180910390fd5b505b604051630b135d3f60e11b808252906001600160a01b038a1690631626ba7e90620001c4908b90869060040162000661565b602060405180830381865afa158015620001e2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200020891906200069d565b6001600160e01b031916149450505050506200044a565b805115620002b157604051630b135d3f60e11b808252906001600160a01b03871690631626ba7e9062000259908890889060040162000661565b602060405180830381865afa15801562000277573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200029d91906200069d565b6001600160e01b031916149150506200044a565b8251604114620003195760405162461bcd60e51b815260206004820152603a6024820152600080516020620006e083398151915260448201527f3a20696e76616c6964207369676e6174757265206c656e677468000000000000606482015260840162000187565b620003236200046b565b506020830151604080850151855186939260009185919081106200034b576200034b620006c9565b016020015160f81c9050601b81148015906200036b57508060ff16601c14155b15620003cf5760405162461bcd60e51b815260206004820152603b6024820152600080516020620006e083398151915260448201527f3a20696e76616c6964207369676e617475726520762076616c75650000000000606482015260840162000187565b6040805160008152602081018083528a905260ff83169181019190915260608101849052608081018390526001600160a01b038a169060019060a0016020604051602081039080840390855afa1580156200042e573d6000803e3d6000fd5b505050602060405103516001600160a01b031614955050505050505b9392505050565b60006020825110156200046357600080fd5b508051015190565b60405180606001604052806003906020820280368337509192915050565b6001600160a01b03811681146200049f57600080fd5b50565b634e487b7160e01b600052604160045260246000fd5b60005b83811015620004d5578181015183820152602001620004bb565b50506000910152565b600082601f830112620004f057600080fd5b81516001600160401b03808211156200050d576200050d620004a2565b604051601f8301601f19908116603f01168101908282118183101715620005385762000538620004a2565b816040528381528660208588010111156200055257600080fd5b62000565846020830160208901620004b8565b9695505050505050565b6000806000606084860312156200058557600080fd5b8351620005928162000489565b6020850151604086015191945092506001600160401b03811115620005b657600080fd5b620005c486828701620004de565b9150509250925092565b600080600060608486031215620005e457600080fd5b8351620005f18162000489565b60208501519093506001600160401b03808211156200060f57600080fd5b6200061d87838801620004de565b935060408601519150808211156200063457600080fd5b50620005c486828701620004de565b6000825162000657818460208701620004b8565b9190910192915050565b828152604060208201526000825180604084015262000688816060850160208701620004b8565b601f01601f1916919091016060019392505050565b600060208284031215620006b057600080fd5b81516001600160e01b0319811681146200044a57600080fd5b634e487b7160e01b600052603260045260246000fdfe5369676e617475726556616c696461746f72237265636f7665725369676e6572" + +export const verifyEIP6492Signature = async ({ + signer, + hash, + signature, + client +}: VerifyEIP6492SignatureParams): Promise => { + const result = await client.call({ + data: concat([ + universalValidatorByteCode, + encodeAbiParameters(parseAbiParameters("address, bytes32, bytes"), [ + signer, + hash, + signature + ]) + ]) + }) + + return result.data === "0x01" +} diff --git a/packages/core/accounts/utils/index.ts b/packages/core/accounts/utils/index.ts index 5e5f1795..33ab4e61 100644 --- a/packages/core/accounts/utils/index.ts +++ b/packages/core/accounts/utils/index.ts @@ -1,2 +1,5 @@ import { toKernelPluginManager } from "./toKernelPluginManager.js" export { toKernelPluginManager } + +import { verifyEIP6492Signature } from "./6492.js" +export { verifyEIP6492Signature } diff --git a/packages/core/constants.ts b/packages/core/constants.ts index f061435a..6de4e629 100644 --- a/packages/core/constants.ts +++ b/packages/core/constants.ts @@ -9,4 +9,5 @@ export const KernelImplToVersionMap: { [key: Address]: string } = { "0xD3F582F6B4814E989Ee8E96bc3175320B5A540ab": "0.2.3" } export const TOKEN_ACTION = "0x2087C7FfD0d0DAE80a00EE74325aBF3449e0eaf1" +export const KERNEL_NAME = "Kernel" export const LATEST_KERNEL_VERSION = "0.2.3" diff --git a/packages/core/index.ts b/packages/core/index.ts index 38314b6d..76003ff1 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -38,3 +38,4 @@ export { gasTokenAddresses, type TokenSymbolsMap } from "./gasTokenAddresses.js" +export { verifyEIP6492Signature } from "./accounts/utils" diff --git a/packages/core/package.json b/packages/core/package.json index 5df20c16..73293235 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@zerodev/sdk", - "version": "5.1.0", + "version": "5.1.1", "author": "ZeroDev", "main": "./_cjs/index.js", "module": "./_esm/index.js", diff --git a/packages/test/ecdsaKernelAccount.test.ts b/packages/test/ecdsaKernelAccount.test.ts index 14c371ce..00a33085 100644 --- a/packages/test/ecdsaKernelAccount.test.ts +++ b/packages/test/ecdsaKernelAccount.test.ts @@ -7,7 +7,8 @@ import { KernelAccountClient, KernelSmartAccount, createKernelAccount, - getERC20PaymasterApproveCall + getERC20PaymasterApproveCall, + verifyEIP6492Signature } from "@zerodev/sdk" import { gasTokenAddresses } from "@zerodev/sdk" import dotenv from "dotenv" @@ -32,13 +33,14 @@ import { hashTypedData, zeroAddress } from "viem" -import { privateKeyToAccount } from "viem/accounts" +import { privateKeyToAccount, sign } from "viem/accounts" import { goerli } from "viem/chains" import { EntryPointAbi } from "./abis/EntryPoint.js" import { GreeterAbi, GreeterBytecode } from "./abis/Greeter.js" import { TEST_ERC20Abi } from "./abis/Test_ERC20Abi.js" import { findUserOperationEvent, + getEcdsaKernelAccountWithRandomSigner, getEntryPoint, getKernelAccountClient, getKernelBundlerClient, @@ -122,6 +124,95 @@ describe("ECDSA kernel Account", () => { }).toThrow(new SignTransactionNotSupportedBySmartAccount()) }) + test("Should validate message signatures for undeployed accounts (6492)", async () => { + const account = await getEcdsaKernelAccountWithRandomSigner() + const message = "hello world" + const signature = await account.signMessage({ + message + }) + + expect( + await verifyEIP6492Signature({ + signer: account.address, + hash: hashMessage(message), + signature: signature, + client: publicClient + }) + ).toBeTrue() + + // Try using Ambire as well + const ambireResult = await verifyMessage({ + signer: account.address, + message, + signature: signature, + provider: new ethers.providers.JsonRpcProvider( + process.env.RPC_URL as string + ) + }) + expect(ambireResult).toBeTrue() + }) + + test("Should validate typed data signatures for undeployed accounts (6492)", async () => { + const domain = { + chainId: 1, + name: "Test", + verifyingContract: zeroAddress + } + + const primaryType = "Test" + + const types = { + Test: [ + { + name: "test", + type: "string" + } + ] + } + + const message = { + test: "hello world" + } + const typedHash = hashTypedData({ + domain, + primaryType, + types, + message + }) + + const account = await getEcdsaKernelAccountWithRandomSigner() + const signature = await account.signTypedData({ + domain, + primaryType, + types, + message + }) + + expect( + await verifyEIP6492Signature({ + signer: account.address, + hash: typedHash, + signature: signature, + client: publicClient + }) + ).toBeTrue() + + // Try using Ambire as well + const ambireResult = await verifyMessage({ + signer: account.address, + typedData: { + domain, + types, + message + }, + signature: signature, + provider: new ethers.providers.JsonRpcProvider( + process.env.RPC_URL as string + ) + }) + expect(ambireResult).toBeTrue() + }) + test( "Client signMessage should return a valid signature", async () => { diff --git a/packages/test/utils.ts b/packages/test/utils.ts index a50a2e4e..9c5cd317 100644 --- a/packages/test/utils.ts +++ b/packages/test/utils.ts @@ -106,24 +106,36 @@ export const getSignerToEcdsaKernelAccount = throw new Error("TEST_PRIVATE_KEY environment variable not set") } - const publicClient = await getPublicClient() - const signer = privateKeyToAccount(privateKey) - const ecdsaValidatorPlugin = await signerToEcdsaValidator( - publicClient, - { - entryPoint: getEntryPoint(), - signer: { ...signer, source: "local" as "local" | "external" } - } - ) + return getEcdsaKernelAccountWithPrivateKey(privateKey) + } - return createKernelAccount(publicClient, { - entryPoint: getEntryPoint(), - plugins: { - sudo: ecdsaValidatorPlugin - } - }) +export const getEcdsaKernelAccountWithRandomSigner = + async (): Promise => { + return getEcdsaKernelAccountWithPrivateKey(generatePrivateKey()) } +const getEcdsaKernelAccountWithPrivateKey = async ( + privateKey: Hex +): Promise => { + if (!privateKey) { + throw new Error("privateKey cannot be empty") + } + + const publicClient = await getPublicClient() + const signer = privateKeyToAccount(privateKey) + const ecdsaValidatorPlugin = await signerToEcdsaValidator(publicClient, { + entryPoint: getEntryPoint(), + signer: { ...signer, source: "local" as "local" | "external" } + }) + + return createKernelAccount(publicClient, { + entryPoint: getEntryPoint(), + plugins: { + sudo: ecdsaValidatorPlugin + } + }) +} + // we only use two signers for testing export const getSignersToWeightedEcdsaKernelAccount = async ( plugin?: KernelValidator