From 25c679524c1f0821b4b1d753c6e76c573eab8384 Mon Sep 17 00:00:00 2001 From: howydev <132113803+howydev@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:20:08 -0500 Subject: [PATCH] feat: add native signer, implement signMessage and signTypedData --- .../src/ma-v2/account/nativeSMASigner.ts | 111 ++++++++++++++++++ .../src/ma-v2/account/semiModularAccountV2.ts | 19 ++- .../src/ma-v2/client/client.test.ts | 58 ++++++++- .../single-signer-validation/signer.ts | 64 +++++++--- .../smart-contracts/src/ma-v2/utils.ts | 31 ++++- 5 files changed, 258 insertions(+), 25 deletions(-) create mode 100644 account-kit/smart-contracts/src/ma-v2/account/nativeSMASigner.ts diff --git a/account-kit/smart-contracts/src/ma-v2/account/nativeSMASigner.ts b/account-kit/smart-contracts/src/ma-v2/account/nativeSMASigner.ts new file mode 100644 index 0000000000..ee113c3779 --- /dev/null +++ b/account-kit/smart-contracts/src/ma-v2/account/nativeSMASigner.ts @@ -0,0 +1,111 @@ +import type { SmartAccountSigner } from "@aa-sdk/core"; +import { + hashMessage, + hashTypedData, + type Hex, + type SignableMessage, + type TypedData, + type TypedDataDefinition, + type Chain, + type Address, +} from "viem"; + +import { packUOSignature, pack1271Signature } from "../utils.js"; +/** + * Creates an object with methods for generating a dummy signature, signing user operation hashes, signing messages, and signing typed data. + * + * @example + * ```ts + * import { singleSignerMessageSigner } from "@account-kit/smart-contracts"; + + * import { LocalAccountSigner } from "@aa-sdk/core"; + * + * const MNEMONIC = "...": + * + * const account = createSMAV2Account({ config }); + * + * const signer = LocalAccountSigner.mnemonicToAccountSigner(MNEMONIC); + * + * const messageSigner = singleSignerMessageSigner(signer, chain); + * ``` + * + * @param {TSigner} signer Signer to use for signing operations + * @param {Chain} chain Chain object for the signer + * @param {Address} accountAddress address of the smart account using this signer + * @param {number} entityId the entity id of the signing validation + * @returns {object} an object with methods for signing operations and managing signatures + */ +export const nativeSMASigner = ( + signer: TSigner, + chain: Chain, + accountAddress: Address, + entityId: number +) => { + const apply712MessageWrap = async (digest: Hex): Promise => { + return hashTypedData({ + domain: { + chainId: Number(chain.id), + verifyingContract: accountAddress, + }, + types: { + ReplaySafeHash: [{ name: "digest", type: "bytes32" }], + }, + message: { + digest, + }, + primaryType: "ReplaySafeHash", + }); + }; + + return { + getDummySignature: (): Hex => { + const dummyEcdsaSignature = + "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; + + return packUOSignature({ + // orderedHookData: [], + validationSignature: dummyEcdsaSignature, + }); + }, + + signUserOperationHash: (uoHash: Hex): Promise => { + return signer.signMessage({ raw: uoHash }).then((signature: Hex) => + packUOSignature({ + // orderedHookData: [], + validationSignature: signature, + }) + ); + }, + + // we apply the expected 1271 packing here since the account contract will expect it + async signMessage({ + message, + }: { + message: SignableMessage; + }): Promise<`0x${string}`> { + const digest = await apply712MessageWrap(hashMessage(message)); + + return pack1271Signature({ + validationSignature: await signer.signMessage({ raw: digest }), + entityId, + }); + }, + + // we don't apply the expected 1271 packing since deferred sigs use typed data sigs and don't expect the 1271 packing + signTypedData: async < + const typedData extends TypedData | Record, + primaryType extends keyof typedData | "EIP712Domain" = keyof typedData + >( + typedDataDefinition: TypedDataDefinition + ): Promise => { + // the accounts domain already gives replay protection across accounts for deferred actions, so we don't need to apply another wrapping + const isDeferredAction = + typedDataDefinition?.primaryType === "DeferredAction" && + typedDataDefinition?.domain?.verifyingContract === accountAddress; + const digest = isDeferredAction + ? hashTypedData(typedDataDefinition) + : await apply712MessageWrap(hashTypedData(typedDataDefinition)); + return signer.signMessage({ raw: digest }); + }, + }; +}; diff --git a/account-kit/smart-contracts/src/ma-v2/account/semiModularAccountV2.ts b/account-kit/smart-contracts/src/ma-v2/account/semiModularAccountV2.ts index 090fc7b11d..4a82acbb38 100644 --- a/account-kit/smart-contracts/src/ma-v2/account/semiModularAccountV2.ts +++ b/account-kit/smart-contracts/src/ma-v2/account/semiModularAccountV2.ts @@ -11,6 +11,7 @@ import { toSmartContractAccount, InvalidEntityIdError, InvalidNonceKeyError, + getAccountAddress, } from "@aa-sdk/core"; import { concatHex, @@ -30,6 +31,7 @@ import { DEFAULT_OWNER_ENTITY_ID, } from "../utils.js"; import { singleSignerMessageSigner } from "../modules/single-signer-validation/signer.js"; +import { nativeSMASigner } from "./nativeSMASigner.js"; import { modularAccountAbi } from "../abis/modularAccountAbi.js"; import { serializeModuleEntity } from "../actions/common/utils.js"; @@ -175,16 +177,25 @@ export async function createSMAV2Account( }) ); + const _accountAddress = await getAccountAddress({ + client, + entryPoint, + accountAddress, + getAccountInitCode, + }); + const baseAccount = await toSmartContractAccount({ transport, chain, entryPoint, - accountAddress, + accountAddress: _accountAddress, source: `SMAV2Account`, encodeExecute, encodeBatchExecute, getAccountInitCode, - ...singleSignerMessageSigner(signer), + ...(entityId === DEFAULT_OWNER_ENTITY_ID + ? nativeSMASigner(signer, chain, _accountAddress, entityId) + : singleSignerMessageSigner(signer, chain, _accountAddress, entityId)), }); // TODO: add deferred action flag @@ -205,13 +216,13 @@ export async function createSMAV2Account( (isGlobalValidation ? 1n : 0n); return entryPointContract.read.getNonce([ - baseAccount.address, + _accountAddress, fullNonceKey, ]) as Promise; }; const accountContract = getContract({ - address: baseAccount.address, + address: _accountAddress, abi: modularAccountAbi, client, }); diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 31e59d228c..3bd3097602 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -1,4 +1,11 @@ -import { custom, parseEther, publicActions } from "viem"; +import { + custom, + parseEther, + publicActions, + getContract, + keccak256, + toHex, +} from "viem"; import { LocalAccountSigner, type SmartAccountSigner } from "@aa-sdk/core"; import { createSMAV2AccountClient, type SMAV2AccountClient } from "./client.js"; import { local070Instance } from "~test/instances.js"; @@ -7,6 +14,7 @@ import { accounts } from "~test/constants.js"; import { getDefaultSingleSignerValidationModuleAddress } from "../modules/utils.js"; import { SingleSignerValidationModule } from "../modules/single-signer-validation/module.js"; import { installValidationActions } from "../actions/install-validation/installValidation.js"; +import { semiModularAccountBytecodeAbi } from "../abis/semiModularAccountBytecodeAbi.js"; describe("MA v2 Tests", async () => { const instance = local070Instance; @@ -182,6 +190,54 @@ describe("MA v2 Tests", async () => { ).rejects.toThrowError(); }); + it("successfully sign + validate a message for native signer", async () => { + const provider = (await givenConnectedProvider({ signer })).extend( + installValidationActions + ); + + const accountContract = getContract({ + address: provider.getAddress(), + abi: semiModularAccountBytecodeAbi, + client, + }); + + // UO deploys the account to test 1271 against + await provider.installValidation({ + validationConfig: { + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), + entityId: 1, + isGlobal: true, + isSignatureValidation: true, + isUserOpValidation: true, + }, + selectors: [], + installData: SingleSignerValidationModule.encodeOnInstallData({ + entityId: 1, + signer: await sessionKey.getAddress(), + }), + hooks: [], + }); + + const message = keccak256(toHex("testmessage")); + + const signature = await provider.signMessage({ message }); + + console.log(await provider.account.isAccountDeployed()); + + console.log( + await accountContract.read.isValidSignature([message, signature]) + ); + + // await expect( + // accountContract.read.isValidSignature({ + // message, + // signature, + // }) + // ).resolves.toBeTruthy(); + }); + const givenConnectedProvider = async ({ signer, accountAddress, diff --git a/account-kit/smart-contracts/src/ma-v2/modules/single-signer-validation/signer.ts b/account-kit/smart-contracts/src/ma-v2/modules/single-signer-validation/signer.ts index 6c64e5e5a7..b61feade1a 100644 --- a/account-kit/smart-contracts/src/ma-v2/modules/single-signer-validation/signer.ts +++ b/account-kit/smart-contracts/src/ma-v2/modules/single-signer-validation/signer.ts @@ -6,36 +6,65 @@ import { type SignableMessage, type TypedData, type TypedDataDefinition, + type Chain, + type Address, } from "viem"; +import { getDefaultSingleSignerValidationModuleAddress } from "../utils.js"; -import { packSignature } from "../../utils.js"; +import { packUOSignature, pack1271Signature } from "../../utils.js"; /** * Creates an object with methods for generating a dummy signature, signing user operation hashes, signing messages, and signing typed data. * - * @example + * @example + * ```ts * import { singleSignerMessageSigner } from "@account-kit/smart-contracts"; * import { LocalAccountSigner } from "@aa-sdk/core"; * * const MNEMONIC = "...": + * + * const account = createSMAV2Account({ config }); * * const signer = LocalAccountSigner.mnemonicToAccountSigner(MNEMONIC); * * const messageSigner = singleSignerMessageSigner(signer, chain); * ``` * - * @param {TSigner} signer the signer to use for signing operations + * @param {TSigner} signer Signer to use for signing operations + * @param {Chain} chain Chain object for the signer + * @param {Address} accountAddress address of the smart account using this signer + * @param {number} entityId the entity id of the signing validation * @returns {object} an object with methods for signing operations and managing signatures */ export const singleSignerMessageSigner = ( - signer: TSigner + signer: TSigner, + chain: Chain, + accountAddress: Address, + entityId: number ) => { + const apply712MessageWrap = async (digest: Hex): Promise => { + return hashTypedData({ + domain: { + chainId: Number(chain.id), + salt: accountAddress, + verifyingContract: getDefaultSingleSignerValidationModuleAddress(chain), + }, + types: { + ReplaySafeHash: [{ name: "digest", type: "bytes32" }], + }, + message: { + digest, + }, + primaryType: "ReplaySafeHash", + }); + }; + return { getDummySignature: (): Hex => { const dummyEcdsaSignature = "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; - return packSignature({ + return packUOSignature({ // orderedHookData: [], validationSignature: dummyEcdsaSignature, }); @@ -43,32 +72,39 @@ export const singleSignerMessageSigner = ( signUserOperationHash: (uoHash: Hex): Promise => { return signer.signMessage({ raw: uoHash }).then((signature: Hex) => - packSignature({ + packUOSignature({ // orderedHookData: [], validationSignature: signature, }) ); }, - // TODO: we can't implement these methods yet, because the RI at `alpha.0` doesn't have a wrapping type, - // and viem doesn't support raw signing, only via EIP-191 or EIP-712. - // When we do implement this, we need to prefix the data with the validation module address & entityId. - - signMessage({ + // we apply the expected 1271 packing here since the account contract will expect it + async signMessage({ message, }: { message: SignableMessage; }): Promise<`0x${string}`> { - return signer.signMessage({ raw: hashMessage(message) }); + const digest = await apply712MessageWrap(hashMessage(message)); + + return pack1271Signature({ + validationSignature: await signer.signMessage({ raw: digest }), + entityId, + }); }, - signTypedData: < + // we don't apply the expected 1271 packing since deferred sigs use typed data sigs and don't expect the 1271 packing + signTypedData: async < const typedData extends TypedData | Record, primaryType extends keyof typedData | "EIP712Domain" = keyof typedData >( typedDataDefinition: TypedDataDefinition ): Promise => { - return signer.signMessage({ raw: hashTypedData(typedDataDefinition) }); + const digest = await apply712MessageWrap( + hashTypedData(typedDataDefinition) + ); + + return signer.signMessage({ raw: digest }); }, }; }; diff --git a/account-kit/smart-contracts/src/ma-v2/utils.ts b/account-kit/smart-contracts/src/ma-v2/utils.ts index 73e241b70b..5bcf18fa76 100644 --- a/account-kit/smart-contracts/src/ma-v2/utils.ts +++ b/account-kit/smart-contracts/src/ma-v2/utils.ts @@ -1,4 +1,4 @@ -import { concat, type Hex, type Chain, type Address } from "viem"; +import { concat, toHex, type Hex, type Chain, type Address } from "viem"; import { arbitrum, arbitrumSepolia, @@ -14,19 +14,38 @@ import { export const DEFAULT_OWNER_ENTITY_ID = 0; -export type PackSignatureParams = { +export type PackUOSignatureParams = { // orderedHookData: HookData[]; validationSignature: Hex; }; -// Signature packing utility -export const packSignature = ({ - // orderedHookData, TO DO: integrate in next iteration of MAv2 sdk +// TODO: direct call validation 1271 +export type Pack1271SignatureParams = { + validationSignature: Hex; + entityId: number; +}; + +// Signature packing utility for user operations +export const packUOSignature = ({ + // orderedHookData, TODO: integrate in next iteration of MAv2 sdk validationSignature, -}: PackSignatureParams): Hex => { +}: PackUOSignatureParams): Hex => { return concat(["0xFF", "0x00", validationSignature]); }; +// Signature packing utility for 1271 signatures +export const pack1271Signature = ({ + validationSignature, + entityId, +}: Pack1271SignatureParams): Hex => { + return concat([ + "0x00", + toHex(entityId, { size: 4 }), + "0xFF", + validationSignature, + ]); +}; + export const getDefaultMAV2FactoryAddress = (chain: Chain): Address => { switch (chain.id) { // TODO: case mekong.id: