Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add install/uninstall validation #1221

Merged
merged 5 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions aa-sdk/core/src/errors/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,19 @@ export class InvalidNonceKeyError extends BaseError {
super(`Nonce key is ${nonceKey} but has to be less than 2**152`);
}
}

/**
* Error class denoting that the provided entity id is invalid because it's overriding the native entity id.
howydev marked this conversation as resolved.
Show resolved Hide resolved
*/
export class EntityIdOverrideError extends BaseError {
override name = "InvalidNonceKeyError";

/**
* Initializes a new instance of the error message with a default message indicating that the nonce key is invalid.
*/
constructor() {
super(
`Installing entityId of 0 overrides the owner's entity id in the account`
);
}
}
1 change: 1 addition & 0 deletions aa-sdk/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export {
InvalidRpcUrlError,
InvalidEntityIdError,
InvalidNonceKeyError,
EntityIdOverrideError,
} from "./errors/client.js";
export {
EntryPointNotFoundError,
Expand Down
126 changes: 121 additions & 5 deletions account-kit/smart-contracts/src/ma-v2/account/semiModularAccountV2.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,78 @@
import type {
EntryPointDef,
SmartAccountSigner,
AccountOp,
SmartContractAccountWithSigner,
ToSmartContractAccountParams,
} from "@aa-sdk/core";
import {
createBundlerClient,
getEntryPoint,
toSmartContractAccount,
InvalidEntityIdError,
InvalidNonceKeyError,
} from "@aa-sdk/core";
import {
concatHex,
encodeFunctionData,
getContract,
maxUint32,
maxUint152,
zeroAddress,
type Address,
type Chain,
type Hex,
type Transport,
} from "viem";
import { accountFactoryAbi } from "../abis/accountFactoryAbi.js";
import { getDefaultMAV2FactoryAddress } from "../utils.js";
import { standardExecutor } from "../../msca/account/standardExecutor.js";
import {
getDefaultMAV2FactoryAddress,
DEFAULT_OWNER_ENTITY_ID,
} from "../utils.js";
import { singleSignerMessageSigner } from "../modules/single-signer-validation/signer.js";
import { InvalidEntityIdError, InvalidNonceKeyError } from "@aa-sdk/core";
import { modularAccountAbi } from "../abis/modularAccountAbi.js";
import { serializeModuleEntity } from "../actions/common/utils.js";

export const DEFAULT_OWNER_ENTITY_ID = 0;
const executeUserOpSelector: Hex = "0x8DD7712F";

export type SignerEntity = {
isGlobalValidation: boolean;
entityId: number;
};

export type ExecutionDataView = {
module: Address;
skipRuntimeValidation: boolean;
allowGlobalValidation: boolean;
executionHooks: readonly Hex[];
};

export type ValidationDataView = {
validationHooks: readonly Hex[];
executionHooks: readonly Hex[];
selectors: readonly Hex[];
validationFlags: number;
};

export type ValidationDataParams =
| {
validationModuleAddress: Address;
entityId?: never;
}
| {
validationModuleAddress?: never;
entityId: number;
};

export type SMAV2Account<
TSigner extends SmartAccountSigner = SmartAccountSigner
> = SmartContractAccountWithSigner<"SMAV2Account", TSigner, "0.7.0"> & {
signerEntity: SignerEntity;
getExecutionData: (selector: Hex) => Promise<ExecutionDataView>;
getValidationData: (
args: ValidationDataParams
) => Promise<ValidationDataView>;
encodeCallData: (callData: Hex) => Promise<Hex>;
};

export type CreateSMAV2AccountParams<
Expand Down Expand Up @@ -110,14 +146,43 @@ export async function createSMAV2Account(
]);
};

const encodeExecute: (tx: AccountOp) => Promise<Hex> = async ({
target,
data,
value,
}) =>
await encodeCallData(
encodeFunctionData({
abi: modularAccountAbi,
functionName: "execute",
args: [target, value ?? 0n, data],
})
);

const encodeBatchExecute: (txs: AccountOp[]) => Promise<Hex> = async (txs) =>
await encodeCallData(
encodeFunctionData({
abi: modularAccountAbi,
functionName: "executeBatch",
args: [
txs.map((tx) => ({
target: tx.target,
data: tx.data,
value: tx.value ?? 0n,
})),
],
})
);

const baseAccount = await toSmartContractAccount({
transport,
chain,
entryPoint,
accountAddress,
source: `SMAV2Account`,
encodeExecute,
encodeBatchExecute,
getAccountInitCode,
...standardExecutor,
...singleSignerMessageSigner(signer),
});

Expand All @@ -144,10 +209,61 @@ export async function createSMAV2Account(
]) as Promise<bigint>;
};

const accountContract = getContract({
address: baseAccount.address,
abi: modularAccountAbi,
client,
});

const getExecutionData = async (selector: Hex) => {
if (!(await baseAccount.isAccountDeployed())) {
return {
module: zeroAddress,
skipRuntimeValidation: false,
allowGlobalValidation: false,
executionHooks: [],
};
}

return await accountContract.read.getExecutionData([selector]);
};

const getValidationData = async (args: ValidationDataParams) => {
if (!(await baseAccount.isAccountDeployed())) {
return {
validationHooks: [],
executionHooks: [],
selectors: [],
validationFlags: 0,
};
}

const { validationModuleAddress, entityId } = args;
return await accountContract.read.getValidationData([
serializeModuleEntity({
moduleAddress: validationModuleAddress ?? zeroAddress,
entityId: entityId ?? Number(maxUint32),
}),
]);
};

const encodeCallData = async (callData: Hex): Promise<Hex> => {
const validationData = await getValidationData({
entityId: Number(entityId),
});

return validationData.executionHooks.length
? concatHex([executeUserOpSelector, callData])
: callData;
};

return {
...baseAccount,
getAccountNonce,
getSigner: () => signer,
signerEntity,
getExecutionData,
getValidationData,
encodeCallData,
};
}
37 changes: 37 additions & 0 deletions account-kit/smart-contracts/src/ma-v2/actions/common/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Address, Hex } from "viem";

export type ModuleEntity = {
moduleAddress: Address;
entityId: number;
};

export type ValidationConfig = {
moduleAddress: Address;
entityId: number; // uint32
isGlobal: boolean;
isSignatureValidation: boolean;
isUserOpValidation: boolean;
};

export enum HookType {
EXECUTION = "0x00",
VALIDATION = "0x01",
}

export type HookConfig = {
address: Address;
entityId: number; // uint32
hookType: HookType;
hasPreHooks: boolean;
hasPostHooks: boolean;
};

// maps to type ValidationStorage in MAv2 implementation
export type ValidationData = {
isGlobal: boolean; // validation flag
isSignatureValidation: boolean; // validation flag
isUserOpValidation: boolean;
validationHooks: HookConfig[];
executionHooks: Hex[];
selectors: Hex[];
};
32 changes: 32 additions & 0 deletions account-kit/smart-contracts/src/ma-v2/actions/common/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { type Hex, toHex, concatHex } from "viem";
import type { ValidationConfig, HookConfig, ModuleEntity } from "./types";
import { HookType } from "./types.js";

export function serializeValidationConfig(config: ValidationConfig): Hex {
const isUserOpValidationBit = config.isUserOpValidation ? 1 : 0;
const isSignatureValidationBit = config.isSignatureValidation ? 2 : 0;
const isGlobalBit = config.isGlobal ? 4 : 0;
return concatHex([
serializeModuleEntity(config),
toHex(isUserOpValidationBit + isSignatureValidationBit + isGlobalBit, {
size: 1,
}),
]);
}

export function serializeHookConfig(config: HookConfig): Hex {
const hookTypeBit = config.hookType === HookType.VALIDATION ? 1 : 0;
const hasPostHooksBit = config.hasPostHooks ? 2 : 0;
const hasPreHooksBit = config.hasPreHooks ? 4 : 0;
return concatHex([
config.address,
toHex(config.entityId, { size: 4 }),
toHex(hookTypeBit + hasPostHooksBit + hasPreHooksBit, {
size: 1,
}),
]);
}

export function serializeModuleEntity(config: ModuleEntity): Hex {
return concatHex([config.moduleAddress, toHex(config.entityId, { size: 4 })]);
}
Loading
Loading