Repository: alchemyplatform/aa-sdk
Pull request #1524: refactor: cleanup and refactor permission builder errors
Author: login: Zer0dot
Created: 2025-04-08T20:52:26Z | Merged: 2025-04-09T00:49:09Z

# Pull Request Checklist

- [ ] Did you add new tests and confirm existing tests pass? (`yarn test`)
- [ ] Did you update relevant docs? (docs are found in the `site` folder, and guidelines for updating/adding docs can be found in the [contribution guide](https://github.com/alchemyplatform/aa-sdk/blob/main/CONTRIBUTING.md))
- [ ] Do your commits follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard?
- [ ] Does your PR title also follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard?
- [ ] If you have a breaking change, is it [correctly reflected in your commit message](https://www.conventionalcommits.org/en/v1.0.0/#examples)? (e.g. `feat!: breaking change`)
- [ ] Did you run lint (`yarn lint:check`) and fix any issues? (`yarn lint:write`)
- [ ] Did you follow the [contribution guidelines](https://github.com/alchemyplatform/aa-sdk/blob/main/CONTRIBUTING.md)?

Also includes a minor fix where the permission builder was not adding the permission to the array with the ACCOUNT_FUNCTIONS permission, leading to potentially duplicating this + root permissions.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `PermissionBuilder` functionality by introducing error handling for various permission-related issues and adding a new utility function, `buildDeferredActionDigest`, to facilitate action digest creation.

### Detailed summary
- Added `buildDeferredActionDigest` function to `utils.ts` for digest creation.
- Introduced error classes in `permissionBuilderErrors.ts` for better error handling.
- Replaced error messages in `PermissionBuilder` with specific error classes.
- Exported new error classes from `permissionBuilderErrors.js`.
- Updated tests to use `buildDeferredActionDigest` instead of `provider.buildDeferredActionDigest`.
- Added unit tests for new error handling scenarios in `deferralActions.test.ts`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->


Diff:
diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts
index e67a19b684..14e2096c3c 100644
--- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts
+++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts
@@ -3,15 +3,18 @@ import {
   LocalAccountSigner,
   type SmartAccountSigner,
 } from "@aa-sdk/core";
-import { custom, parseEther } from "viem";
+import { custom, parseEther, toHex, type Address, type Hex } from "viem";
 import {
   createModularAccountV2Client,
   type SignerEntity,
 } from "@account-kit/smart-contracts";
 import {
+  buildDeferredActionDigest,
   deferralActions,
   PermissionBuilder,
   PermissionType,
+  RootPermissionOnlyError,
+  type Permission,
 } from "@account-kit/smart-contracts/experimental";
 import { local070Instance } from "~test/instances.js";
 import { setBalance } from "viem/actions";
@@ -75,7 +78,7 @@ describe("MA v2 deferral actions tests", async () => {
 
     const sig = await provider.account.signTypedData(typedData);
 
-    const deferredActionDigest = await serverClient.buildDeferredActionDigest({
+    const deferredActionDigest = buildDeferredActionDigest({
       fullPreSignatureDeferredActionDigest,
       sig,
     });
@@ -100,6 +103,114 @@ describe("MA v2 deferral actions tests", async () => {
     await provider.waitForUserOperationTransaction(uoResult);
   });
 
+  it("PermissionBuilder: Cannot add any permission after root", async () => {
+    const provider = await givenConnectedProvider({ signer });
+
+    const serverClient = (
+      await createModularAccountV2Client({
+        chain: instance.chain,
+        accountAddress: provider.getAddress(),
+        signer: new LocalAccountSigner(accounts.fundedAccountOwner),
+        transport: custom(instance.getClient()),
+      })
+    ).extend(deferralActions);
+
+    await setBalance(instance.getClient(), {
+      address: provider.getAddress(),
+      value: parseEther("2"),
+    });
+
+    const sessionKey: SmartAccountSigner = new LocalAccountSigner(
+      accounts.unfundedAccountOwner
+    );
+
+    // these can be default values or from call arguments
+    const { entityId, nonce } = await serverClient.getEntityIdAndNonce({
+      isGlobalValidation: true,
+    });
+
+    const allPermissions = generateAllPermissions();
+
+    allPermissions.forEach((permission) => {
+      expect(async () => {
+        new PermissionBuilder({
+          client: serverClient,
+          key: {
+            publicKey: await sessionKey.getAddress(),
+            type: "secp256k1",
+          },
+          entityId,
+          nonce: nonce,
+          deadline: 0,
+        })
+          .addPermission({
+            permission: {
+              type: PermissionType.ROOT,
+            },
+          })
+          .addPermission({ permission });
+      }).rejects.toThrow(new RootPermissionOnlyError(permission));
+    });
+  });
+
+  it("PermissionBuilder: Cannot add root after any permission", async () => {
+    const provider = await givenConnectedProvider({ signer });
+
+    const serverClient = (
+      await createModularAccountV2Client({
+        chain: instance.chain,
+        accountAddress: provider.getAddress(),
+        signer: new LocalAccountSigner(accounts.fundedAccountOwner),
+        transport: custom(instance.getClient()),
+      })
+    ).extend(deferralActions);
+
+    await setBalance(instance.getClient(), {
+      address: provider.getAddress(),
+      value: parseEther("2"),
+    });
+
+    const sessionKey: SmartAccountSigner = new LocalAccountSigner(
+      accounts.unfundedAccountOwner
+    );
+
+    // these can be default values or from call arguments
+    const { entityId, nonce } = await serverClient.getEntityIdAndNonce({
+      isGlobalValidation: true,
+    });
+
+    const allPermissions = generateAllPermissions();
+
+    allPermissions.forEach((permission) => {
+      expect(async () => {
+        return new PermissionBuilder({
+          client: serverClient,
+          key: {
+            publicKey: await sessionKey.getAddress(),
+            type: "secp256k1",
+          },
+          entityId,
+          nonce: nonce,
+          deadline: 0,
+        })
+          .addPermission({ permission })
+          .addPermission({
+            permission: {
+              type: PermissionType.ROOT,
+            },
+          });
+      }).rejects.toThrow(
+        new RootPermissionOnlyError({
+          type: PermissionType.ROOT,
+        })
+      );
+    });
+  });
+
+  /* -------------------------------------------------------------------------- */
+  /*                                  Utilities                                 */
+  /* -------------------------------------------------------------------------- */
+
   const givenConnectedProvider = async ({
     signer,
     signerEntity,
@@ -130,4 +241,168 @@ describe("MA v2 deferral actions tests", async () => {
         : {}),
       salt,
     });
+
+  /**
+   * Creates a sample ERC20 allowance permission
+   *
+   * @param {Address} tokenAddress The address of the ERC20 token contract
+   * @param {string|number} allowance The allowance amount in wei (as a string or number)
+   * @returns {Permission} An ERC20_TOKEN_TRANSFER permission
+   */
+  function createERC20TokenTransferPermission(
+    tokenAddress: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum mainnet
+    allowance: string | number = "1000000000" // 1,000 USDC with 6 decimals
+  ): Permission {
+    return {
+      type: PermissionType.ERC20_TOKEN_TRANSFER,
+      data: {
+        address: tokenAddress,
+        allowance: toHex(BigInt(allowance)),
+      },
+    };
+  }
+
+  /**
+   * Creates a sample native token transfer permission
+   *
+   * @param {string|number} allowance The allowance amount in wei (as a string or number)
+   * @returns {Permission} A NATIVE_TOKEN_TRANSFER permission
+   */
+  function createNativeTokenTransferPermission(
+    allowance: string | number = "1000000000000000000" // 1 ETH
+  ): Permission {
+    return {
+      type: PermissionType.NATIVE_TOKEN_TRANSFER,
+      data: {
+        allowance: toHex(BigInt(allowance)),
+      },
+    };
+  }
+
+  /**
+   * Creates a sample gas limit permission
+   *
+   * @param {string|number} limit The gas limit (as a string or number)
+   * @returns {Permission} A GAS_LIMIT permission
+   */
+  function createGasLimitPermission(
+    limit: string | number = "100000" // 100k gas
+  ): Permission {
+    return {
+      type: PermissionType.GAS_LIMIT,
+      data: {
+        limit: toHex(BigInt(limit)),
+      },
+    };
+  }
+
+  /**
+   * Creates a sample contract access permission
+   *
+   * @param {Address} contractAddress The address of the contract to allow access to
+   * @returns {Permission} A CONTRACT_ACCESS permission
+   */
+  function createContractAccessPermission(
+    contractAddress: Address = "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984" // Uniswap on Ethereum mainnet
+  ): Permission {
+    return {
+      type: PermissionType.CONTRACT_ACCESS,
+      data: {
+        address: contractAddress,
+      },
+    };
+  }
+
+  /**
+   * Creates a sample account functions permission
+   *
+   * @param {Hex[]} functionSignatures Array of function signature hexes
+   * @returns {Permission} An ACCOUNT_FUNCTIONS permission
+   */
+  function createAccountFunctionsPermission(
+    functionSignatures: Hex[] = [
+      "0x095ea7b3", // approve(address,uint256)
+      "0xa9059cbb", // transfer(address,uint256)
+      "0x23b872dd", // transferFrom(address,address,uint256)
+    ]
+  ): Permission {
+    return {
+      type: PermissionType.ACCOUNT_FUNCTIONS,
+      data: {
+        functions: functionSignatures,
+      },
+    };
+  }
+
+  /**
+   * Creates a sample functions on all contracts permission
+   *
+   * @param {Hex[]} functionSignatures Array of function signature hexes
+   * @returns {Permission} A FUNCTIONS_ON_ALL_CONTRACTS permission
+   */
+  function createFunctionsOnAllContractsPermission(
+    functionSignatures: Hex[] = [
+      "0x095ea7b3", // approve(address,uint256)
+      "0xa9059cbb", // transfer(address,uint256)
+    ]
+  ): Permission {
+    return {
+      type: PermissionType.FUNCTIONS_ON_ALL_CONTRACTS,
+      data: {
+        functions: functionSignatures,
+      },
+    };
+  }
+
+  /**
+   * Creates a sample functions on specific contract permission
+   *
+   * @param {Address} contractAddress The address of the contract
+   * @param {Hex[]} functionSignatures Array of function signature hexes
+   * @returns {Permission} A FUNCTIONS_ON_CONTRACT permission
+   */
+  function createFunctionsOnContractPermission(
+    contractAddress: Address = "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", // Uniswap on Ethereum mainnet
+    functionSignatures: Hex[] = [
+      "0x7ff36ab5", // swapExactETHForTokens
+      "0x18cbafe5", // swapExactTokensForETH
+    ]
+  ): Permission {
+    return {
+      type: PermissionType.FUNCTIONS_ON_CONTRACT,
+      data: {
+        address: contractAddress,
+        functions: functionSignatures,
+      },
+    };
+  }
+
+  /**
+   * Creates a root permission
+   *
+   * @returns {Permission} A ROOT permission
+   */
+  function createRootPermission(): Permission {
+    return {
+      type: PermissionType.ROOT,
+    };
+  }
+
+  /**
+   * Generates one of each permission type
+   *
+   * @returns {Permission[]} An array containing one of each permission type
+   */
+  function generateAllPermissions(): Permission[] {
+    return [
+      createNativeTokenTransferPermission(),
+      createERC20TokenTransferPermission(),
+      createGasLimitPermission(),
+      createContractAccessPermission(),
+      createAccountFunctionsPermission(),
+      createFunctionsOnAllContractsPermission(),
+      createFunctionsOnContractPermission(),
+      createRootPermission(),
+    ];
+  }
 });
diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts
index b1a035f614..a9c7a247b5 100644
--- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts
+++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts
@@ -51,11 +51,6 @@ export type CreateDeferredActionTypedDataParams = {
   nonce: bigint;
 };
 
-export type BuildDeferredActionDigestParams = {
-  fullPreSignatureDeferredActionDigest: Hex;
-  sig: Hex;
-};
-
 export type BuildPreSignatureDeferredActionDigestParams = {
   typedData: DeferredActionTypedData;
 };
@@ -77,7 +72,6 @@ export type DeferralActions = {
   createDeferredActionTypedDataObject: (
     args: CreateDeferredActionTypedDataParams
   ) => Promise<DeferredActionReturnData>;
-  buildDeferredActionDigest: (args: BuildDeferredActionDigestParams) => Hex;
   buildPreSignatureDeferredActionDigest: (
     args: BuildPreSignatureDeferredActionDigestParams
   ) => Hex;
@@ -135,30 +129,6 @@ export const deferralActions: (
     };
   };
 
-  /**
-   * Creates the digest which must be prepended to the userOp signature.
-   *
-   * Assumption: The client this extends is used to sign the typed data.
-   *
-   * @param {object} args The argument object containing the following:
-   * @param {Hex} args.fullPreSignatureDeferredActionDigest The The data to append the signature and length to
-   * @param {Hex} args.sig The signature to include in the digest
-   * @returns {Hex} The encoded digest to be prepended to the userOp signature
-   */
-  const buildDeferredActionDigest = ({
-    fullPreSignatureDeferredActionDigest,
-    sig,
-  }: BuildDeferredActionDigestParams): Hex => {
-    const sigLength = size(sig);
-
-    const encodedData = concatHex([
-      fullPreSignatureDeferredActionDigest,
-      toHex(sigLength, { size: 4 }),
-      sig,
-    ]);
-    return encodedData;
-  };
-
   const buildPreSignatureDeferredActionDigest = ({
     typedData,
   }: BuildPreSignatureDeferredActionDigestParams): Hex => {
@@ -270,7 +240,6 @@ export const deferralActions: (
 
   return {
     createDeferredActionTypedDataObject,
-    buildDeferredActionDigest,
     buildPreSignatureDeferredActionDigest,
     buildUserOperationWithDeferredAction,
     getEntityIdAndNonce,
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 e31ad17bc2..4782fb6304 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
@@ -68,7 +68,11 @@ import {
 } from "@account-kit/infra";
 import { getMAV2UpgradeToData } from "@account-kit/smart-contracts";
 import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
-import { mintableERC20Abi, mintableERC20Bytecode } from "../utils.js";
+import {
+  buildDeferredActionDigest,
+  mintableERC20Abi,
+  mintableERC20Bytecode,
+} from "../utils.js";
 
 // Note: These tests maintain a shared state to not break the local-running rundler by desyncing the chain.
 describe("MA v2 Tests", async () => {
@@ -390,7 +394,7 @@ describe("MA v2 Tests", async () => {
     );
 
     // Build the full hex to prepend to the UO signature
-    const deferredActionDigest = provider.buildDeferredActionDigest({
+    const deferredActionDigest = buildDeferredActionDigest({
       fullPreSignatureDeferredActionDigest,
       sig: deferredValidationSig,
     });
@@ -503,7 +507,7 @@ describe("MA v2 Tests", async () => {
     );
 
     // Build the full hex to prepend to the UO signature
-    const deferredActionDigest = provider.buildDeferredActionDigest({
+    const deferredActionDigest = buildDeferredActionDigest({
       fullPreSignatureDeferredActionDigest,
       sig: deferredValidationSig,
     });
@@ -606,7 +610,7 @@ describe("MA v2 Tests", async () => {
       });
 
     // Build the full hex to prepend to the UO signature
-    const deferredActionDigest = provider.buildDeferredActionDigest({
+    const deferredActionDigest = buildDeferredActionDigest({
       fullPreSignatureDeferredActionDigest,
       sig: deferredValidationSig,
     });
@@ -734,7 +738,7 @@ describe("MA v2 Tests", async () => {
     );
 
     // Build the full hex to prepend to the UO signature
-    const deferredActionDigest = sessionKeyClient.buildDeferredActionDigest({
+    const deferredActionDigest = buildDeferredActionDigest({
       fullPreSignatureDeferredActionDigest,
       sig: deferredValidationSig,
     });
diff --git a/account-kit/smart-contracts/src/ma-v2/index.ts b/account-kit/smart-contracts/src/ma-v2/index.ts
index 979f13bc36..216305fb0c 100644
--- a/account-kit/smart-contracts/src/ma-v2/index.ts
+++ b/account-kit/smart-contracts/src/ma-v2/index.ts
@@ -24,6 +24,7 @@ export type * from "./actions/deferralActions.js";
 export { deferralActions } from "./actions/deferralActions.js";
 export type * from "./permissionBuilder.js";
 export { PermissionBuilder, PermissionType } from "./permissionBuilder.js";
+export * from "./permissionBuilderErrors.js";
 
 export {
   getDefaultAllowlistModuleAddress,
@@ -33,7 +34,7 @@ export {
   getDefaultTimeRangeModuleAddress,
   getDefaultWebauthnValidationModuleAddress,
 } from "./modules/utils.js";
-export { buildFullNonceKey } from "./utils.js";
+export { buildFullNonceKey, buildDeferredActionDigest } from "./utils.js";
 export { allowlistModuleAbi } from "./modules/allowlist-module/abis/allowlistModuleAbi.js";
 export { AllowlistModule } from "./modules/allowlist-module/module.js";
 export { nativeTokenLimitModuleAbi } from "./modules/native-token-limit-module/abis/nativeTokenLimitModuleAbi.js";
diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts
index 68bc0d018a..e3a74a6491 100644
--- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts
+++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts
@@ -1,4 +1,4 @@
-import { toHex, zeroAddress, type Address, type Hex } from "viem";
+import { maxUint48, toHex, zeroAddress, type Address, type Hex } from "viem";
 import {
   HookType,
   type HookConfig,
@@ -23,6 +23,19 @@ import {
 import { SingleSignerValidationModule } from "./modules/single-signer-validation/module.js";
 import { AllowlistModule } from "./modules/allowlist-module/module.js";
 import { TimeRangeModule } from "./modules/time-range-module/module.js";
+import {
+  AccountAddressAsTargetError,
+  DeadlineOverLimitError,
+  DuplicateTargetAddressError,
+  ExpiredDeadlineError,
+  MultipleGasLimitError,
+  MultipleNativeTokenTransferError,
+  NoFunctionsProvidedError,
+  RootPermissionOnlyError,
+  UnsupportedPermissionTypeError,
+  ValidationConfigUnsetError,
+  ZeroAddressError,
+} from "./permissionBuilderErrors.js";
 
 // We use this to offset the ERC20 spend limit entityId
 const HALF_UINT32 = 2147483647;
@@ -257,9 +270,7 @@ export class PermissionBuilder {
     // Check 1: If we're adding root, we can't have any other permissions
     if (permission.type === PermissionType.ROOT) {
       if (this.permissions.length !== 0) {
-        throw new Error(
-          "PERMISSION: ROOT: Cannot add ROOT permission with other permissions"
-        );
+        throw new RootPermissionOnlyError(permission);
       }
       this.permissions.push(permission);
       // Set isGlobal to true
@@ -272,9 +283,7 @@ export class PermissionBuilder {
     // NOTE: Technically this could be replaced by checking permissions[0] since it should not be possible
     // to have >1 permission with root among them
     if (this.permissions.find((p) => p.type === PermissionType.ROOT)) {
-      throw new Error(
-        `PERMISSION: ${permission.type} => Cannot add permissions with ROOT enabled`
-      );
+      throw new RootPermissionOnlyError(permission);
     }
 
     // Check 3: If the permission is either CONTRACT_ACCESS or FUNCTIONS_ON_CONTRACT, ensure it doesn't collide with another like it.
@@ -284,9 +293,7 @@ export class PermissionBuilder {
     ) {
       // Check 3.1: address must not be the account address, or the user should use the ACCOUNT_FUNCTIONS permission
       if (permission.data.address === this.client.account.address) {
-        throw new Error(
-          `PERMISSION: ${permission.type} => Account address as target, use ACCOUNT_FUNCTIONS for account address`
-        );
+        throw new AccountAddressAsTargetError(permission);
       }
 
       // Check 3.2: there must not be an existing permission with this address as a target
@@ -302,20 +309,19 @@ export class PermissionBuilder {
       );
 
       if (existingPermissionWithSameAddress) {
-        throw new Error(
-          `PERMISSION: ${permission.type} => Address ${targetAddress} already has a permission. Cannot add multiple CONTRACT_ACCESS or FUNCTIONS_ON_CONTRACT permissions for the same target address.`
-        );
+        throw new DuplicateTargetAddressError(permission, targetAddress);
       }
     }
 
     // Check 4: If the permission is ACCOUNT_FUNCTIONS, add selectors
     if (permission.type === PermissionType.ACCOUNT_FUNCTIONS) {
+      if (permission.data.functions.length === 0) {
+        throw new NoFunctionsProvidedError(permission);
+      }
       this.selectors = [...this.selectors, ...permission.data.functions];
-      return this;
     }
 
     this.permissions.push(permission);
-
     return this;
   }
 
@@ -336,16 +342,16 @@ export class PermissionBuilder {
     // Add time range module hook via expiry
     if (this.deadline !== 0) {
       if (this.deadline < Date.now() / 1000) {
-        throw new Error(
-          `PERMISSION: compileDeferred(): deadline ${
-            this.deadline
-          } cannot be before now (${Date.now() / 1000})`
-        );
+        throw new ExpiredDeadlineError(this.deadline, Date.now() / 1000);
+      }
+      if (this.deadline > maxUint48) {
+        throw new DeadlineOverLimitError(this.deadline);
       }
+
       this.hooks.push(
         TimeRangeModule.buildHook(
           {
-            entityId: this.validationConfig.entityId, // will be timerange entityId
+            entityId: this.validationConfig.entityId,
             validUntil: this.deadline,
             validAfter: 0,
           },
@@ -420,9 +426,7 @@ export class PermissionBuilder {
       this.validationConfig.isGlobal === false &&
       this.selectors.length === 0
     ) {
-      throw new Error(
-        "Validation config unset, use permissionBuilder.configure(...)"
-      );
+      throw new ValidationConfigUnsetError();
     }
   }
 
@@ -441,9 +445,7 @@ export class PermissionBuilder {
         case PermissionType.NATIVE_TOKEN_TRANSFER:
           // Should never be added twice, check is on addPermission(s) too
           if (rawHooks[HookIdentifier.NATIVE_TOKEN_TRANSFER] !== undefined) {
-            throw new Error(
-              "PERMISSION: NATIVE_TOKEN_TRANSFER => Must have at most ONE native token transfer permission"
-            );
+            throw new MultipleNativeTokenTransferError(permission);
           }
           rawHooks[HookIdentifier.NATIVE_TOKEN_TRANSFER] = {
             hookConfig: {
@@ -464,9 +466,7 @@ export class PermissionBuilder {
           break;
         case PermissionType.ERC20_TOKEN_TRANSFER:
           if (permission.data.address === zeroAddress) {
-            throw new Error(
-              "PERMISSION: ERC20_TOKEN_TRANSFER => Zero address provided"
-            );
+            throw new ZeroAddressError(permission);
           }
           rawHooks[HookIdentifier.ERC20_TOKEN_TRANSFER] = {
             hookConfig: {
@@ -522,9 +522,7 @@ export class PermissionBuilder {
         case PermissionType.GAS_LIMIT:
           // Should only ever be added once, check is also on addPermission(s)
           if (rawHooks[HookIdentifier.GAS_LIMIT] !== undefined) {
-            throw new Error(
-              "PERMISSION: GAS_LIMIT => Must have at most ONE gas limit permission"
-            );
+            throw new MultipleGasLimitError(permission);
           }
           rawHooks[HookIdentifier.GAS_LIMIT] = {
             hookConfig: {
@@ -544,9 +542,7 @@ export class PermissionBuilder {
           break;
         case PermissionType.CONTRACT_ACCESS:
           if (permission.data.address === zeroAddress) {
-            throw new Error(
-              "PERMISSION: CONTRACT_ACCESS => Zero address provided"
-            );
+            throw new ZeroAddressError(permission);
           }
           rawHooks[HookIdentifier.PREVAL_ALLOWLIST] = {
             hookConfig: {
@@ -574,17 +570,11 @@ export class PermissionBuilder {
           };
           break;
         case PermissionType.ACCOUNT_FUNCTIONS:
-          if (permission.data.functions.length === 0) {
-            throw new Error(
-              "PERMISSION: ACCOUNT_FUNCTION => No functions provided"
-            ); // should be in add perm
-          }
+          // This is handled in add permissions
           break;
         case PermissionType.FUNCTIONS_ON_ALL_CONTRACTS:
           if (permission.data.functions.length === 0) {
-            throw new Error(
-              "PERMISSION: FUNCTIONS_ON_ALL_CONTRACTS => No functions provided"
-            );
+            throw new NoFunctionsProvidedError(permission);
           }
           rawHooks[HookIdentifier.PREVAL_ALLOWLIST] = {
             hookConfig: {
@@ -613,14 +603,10 @@ export class PermissionBuilder {
           break;
         case PermissionType.FUNCTIONS_ON_CONTRACT:
           if (permission.data.functions.length === 0) {
-            throw new Error(
-              "PERMISSION: FUNCTIONS_ON_CONTRACT => No functions provided"
-            );
+            throw new NoFunctionsProvidedError(permission);
           }
           if (permission.data.address === zeroAddress) {
-            throw new Error(
-              "PERMISSION: FUNCTIONS_ON_CONTRACT => Zero address provided"
-            );
+            throw new ZeroAddressError(permission);
           }
           rawHooks[HookIdentifier.PREVAL_ALLOWLIST] = {
             hookConfig: {
@@ -651,9 +637,7 @@ export class PermissionBuilder {
           // Root permission handled in addPermission
           break;
         default:
-          throw new Error(
-            `Unsupported permission type: ${(permission as any).type}`
-          );
+          assertNever(permission);
       }
 
       // isGlobal guaranteed to be false since it's only set with root permissions,
@@ -714,3 +698,7 @@ export class PermissionBuilder {
     }
   }
 }
+
+export function assertNever(_valid: never): never {
+  throw new UnsupportedPermissionTypeError();
+}
diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilderErrors.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilderErrors.ts
new file mode 100644
index 0000000000..03e54ca931
--- /dev/null
+++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilderErrors.ts
@@ -0,0 +1,153 @@
+import { BaseError, type Address } from "@aa-sdk/core";
+import type { Permission } from "./permissionBuilder";
+
+export class RootPermissionOnlyError extends BaseError {
+  override name = "PermissionBuilder: RootPermissionOnlyError";
+
+  /**
+   * Constructor for initializing an error message indicating that an account could not be found to execute the specified action.
+   *
+   * @param {Permission} permission The permission trying to be added atop the root permission
+   */
+  constructor(permission: Permission) {
+    super(`Adding ${permission}: Cannot add permissions with ROOT permission`);
+  }
+}
+
+export class AccountAddressAsTargetError extends BaseError {
+  override name = "PermissionBuilder: AccountAddressAsTargetError";
+
+  /**
+   * Constructor for initializing an error message indicating that account address is used as target.
+   *
+   * @param {Permission} permission The permission with account address as target
+   */
+  constructor(permission: Permission) {
+    super(
+      `${permission.type}: Account address as target, use ACCOUNT_FUNCTIONS for account address`
+    );
+  }
+}
+
+export class DuplicateTargetAddressError extends BaseError {
+  override name = "PermissionBuilder: DuplicateTargetAddressError";
+
+  /**
+   * Constructor for initializing an error message indicating duplicate target address in permissions.
+   *
+   * @param {Permission} permission The permission with duplicate target address
+   * @param {Address} targetAddress The duplicate target address
+   */
+  constructor(permission: Permission, targetAddress: Address) {
+    super(
+      `${permission.type}: Address ${targetAddress} already has a permission. Cannot add multiple CONTRACT_ACCESS or FUNCTIONS_ON_CONTRACT permissions for the same target address.`
+    );
+  }
+}
+
+export class NoFunctionsProvidedError extends BaseError {
+  override name = "PermissionBuilder: NoFunctionsProvidedError";
+
+  /**
+   * Constructor for initializing an error message indicating no functions were provided.
+   *
+   * @param {Permission} permission The permission missing functions
+   */
+  constructor(permission: Permission) {
+    super(`${permission.type}: No functions provided`);
+  }
+}
+
+export class ExpiredDeadlineError extends BaseError {
+  override name = "PermissionBuilder: ExpiredDeadlineError";
+
+  /**
+   * Constructor for initializing an error message indicating the deadline has expired.
+   *
+   * @param {number} deadline The expired deadline timestamp
+   * @param {number} currentTime The current timestamp
+   */
+  constructor(deadline: number, currentTime: number) {
+    super(
+      `compileDeferred(): deadline ${deadline} cannot be before now (${currentTime})`
+    );
+  }
+}
+
+export class DeadlineOverLimitError extends BaseError {
+  override name = "PermissionBuilder: DeadlineOverLimitError";
+
+  /**
+   * Constructor for initializing an error message indicating the deadline has expired.
+   *
+   * @param {number} deadline The expired deadline timestamp
+   */
+  constructor(deadline: number) {
+    super(
+      `compileDeferred(): deadline ${deadline} cannot be > max uint48 (2^48 - 1)`
+    );
+  }
+}
+
+export class ValidationConfigUnsetError extends BaseError {
+  override name = "PermissionBuilder: ValidationConfigUnsetError";
+
+  /**
+   * Constructor for initializing an error message indicating the validation config is unset.
+   */
+  constructor() {
+    super("Validation config unset, use permissionBuilder.configure(...)");
+  }
+}
+
+export class MultipleNativeTokenTransferError extends BaseError {
+  override name = "PermissionBuilder: MultipleNativeTokenTransferError";
+
+  /**
+   * Constructor for initializing an error message indicating multiple native token transfer permissions.
+   *
+   * @param {Permission} permission The duplicate native token transfer permission
+   */
+  constructor(permission: Permission) {
+    super(
+      `${permission.type}: Must have at most ONE native token transfer permission`
+    );
+  }
+}
+
+export class ZeroAddressError extends BaseError {
+  override name = "PermissionBuilder: ZeroAddressError";
+
+  /**
+   * Constructor for initializing an error message indicating zero address was provided.
+   *
+   * @param {Permission} permission The permission with zero address
+   */
+  constructor(permission: Permission) {
+    super(`${permission.type}: Zero address provided`);
+  }
+}
+
+export class MultipleGasLimitError extends BaseError {
+  override name = "PermissionBuilder: MultipleGasLimitError";
+
+  /**
+   * Constructor for initializing an error message indicating multiple gas limit permissions.
+   *
+   * @param {Permission} permission The duplicate gas limit permission
+   */
+  constructor(permission: Permission) {
+    super(`${permission.type}: Must have at most ONE gas limit permission`);
+  }
+}
+
+export class UnsupportedPermissionTypeError extends BaseError {
+  override name = "PermissionBuilder: UnsupportedPermissionTypeError";
+
+  /**
+   * Constructor for initializing an error message indicating an unsupported permission type.
+   */
+  constructor() {
+    super(`Unsupported permission type`);
+  }
+}
diff --git a/account-kit/smart-contracts/src/ma-v2/utils.ts b/account-kit/smart-contracts/src/ma-v2/utils.ts
index 69dd41b72d..739a13ad0a 100644
--- a/account-kit/smart-contracts/src/ma-v2/utils.ts
+++ b/account-kit/smart-contracts/src/ma-v2/utils.ts
@@ -8,6 +8,8 @@ import {
   type Address,
   type Transport,
   parseAbi,
+  size,
+  concatHex,
 } from "viem";
 import {
   arbitrum,
@@ -278,3 +280,31 @@ export const parseDeferredAction = (
     hasAssociatedExecHooks: deferredAction[3] === "1",
   };
 };
+export type BuildDeferredActionDigestParams = {
+  fullPreSignatureDeferredActionDigest: Hex;
+  sig: Hex;
+};
+
+/**
+ * Creates the digest which must be prepended to the userOp signature.
+ *
+ * Assumption: The client this extends is used to sign the typed data.
+ *
+ * @param {object} args The argument object containing the following:
+ * @param {Hex} args.fullPreSignatureDeferredActionDigest The The data to append the signature and length to
+ * @param {Hex} args.sig The signature to include in the digest
+ * @returns {Hex} The encoded digest to be prepended to the userOp signature
+ */
+export const buildDeferredActionDigest = ({
+  fullPreSignatureDeferredActionDigest,
+  sig,
+}: BuildDeferredActionDigestParams): Hex => {
+  const sigLength = size(sig);
+
+  const encodedData = concatHex([
+    fullPreSignatureDeferredActionDigest,
+    toHex(sigLength, { size: 4 }),
+    sig,
+  ]);
+  return encodedData;
+};


Reviews:
user: login: howydev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T21:16:37Z
user: login: howydev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T21:18:30Z
user: login: howydev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T21:19:12Z
user: login: howydev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T21:20:03Z
user: login: howydev
body: lgtm
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T22:09:28Z
user: login: howydev
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T22:14:15Z
user: login: howydev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T22:28:38Z
user: login: howydev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T00:33:21Z

Review comments:
diff_hunk: @@ -336,16 +341,12 @@ export class PermissionBuilder {
     // Add time range module hook via expiry
     if (this.deadline !== 0) {
path: account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts
user: login: howydev
body: we should prob cap this to u32max
created_at: 2025-04-08T21:16:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -464,9 +461,7 @@ export class PermissionBuilder {
           break;
         case PermissionType.ERC20_TOKEN_TRANSFER:
path: account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts
user: login: howydev
body: nit: if we're overwriting the hook config, let's add a check for a `null` hookconfig
created_at: 2025-04-08T21:18:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -464,9 +461,7 @@ export class PermissionBuilder {
           break;
         case PermissionType.ERC20_TOKEN_TRANSFER:
path: account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts
user: login: howydev
body: oh nvm saw we are doing ...
created_at: 2025-04-08T21:19:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -651,9 +632,7 @@ export class PermissionBuilder {
           // Root permission handled in addPermission
           break;
         default:
-          throw new Error(
-            `Unsupported permission type: ${(permission as any).type}`
-          );
+          throw new UnsupportedPermissionTypeError(permission);
path: account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts
user: login: howydev
body: very small nit: maybe use assertNever pattern
created_at: 2025-04-08T21:20:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-08T20:52:30Z
body: [vc]: #DEOjkZ00LikVYfwld4yQiAY4d+8VeoDEJiThkbWJEL0=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJhYS1zZGstc2l0ZSIsInJvb3REaXJlY3RvcnkiOiJzaXRlIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2FsY2hlbXktZG90LWNvbS9hYS1zZGstc2l0ZS83NDVNN3htSlIxaW81ZlgyWENkZUd6WXBUVjVFIiwicHJldmlld1VybCI6ImFhLXNkay1zaXRlLWdpdC16ZXIwZG90LXBlcm1pc3Npb24tYnVpbGQtZDM4NjNlLWFsY2hlbXktZG90LWNvbS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImFhLXNkay1zaXRlLWdpdC16ZXIwZG90LXBlcm1pc3Npb24tYnVpbGQtZDM4NjNlLWFsY2hlbXktZG90LWNvbS52ZXJjZWwuYXBwIn19LHsibmFtZSI6ImFhLXNkay11aS1kZW1vIiwicm9vdERpcmVjdG9yeSI6ImV4YW1wbGVzL3VpLWRlbW8iLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vYWxjaGVteS1kb3QtY29tL2FhLXNkay11aS1kZW1vL0Y5ZG43QnFxR3duaFpDUEx1VjZ5bmh5UHdQd2UiLCJwcmV2aWV3VXJsIjoiYWEtc2RrLXVpLWRlbW8tZ2l0LXplcjBkb3QtcGVybWlzc2lvbi1idS0yMTdhYmItYWxjaGVteS1kb3QtY29tLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiYWEtc2RrLXVpLWRlbW8tZ2l0LXplcjBkb3QtcGVybWlzc2lvbi1idS0yMTdhYmItYWxjaGVteS1kb3QtY29tLnZlcmNlbC5hcHAifX1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **aa-sdk-site** | ✅ Ready ([Inspect](https://vercel.com/alchemy-dot-com/aa-sdk-site/745M7xmJR1io5fX2XCdeGzYpTV5E)) | [Visit Preview](https://aa-sdk-site-git-zer0dot-permission-build-d3863e-alchemy-dot-com.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/aa-sdk-site-git-zer0dot-permission-build-d3863e-alchemy-dot-com.vercel.app?via=pr-comment-feedback-link) | Apr 9, 2025 0:47am |
| **aa-sdk-ui-demo** | ✅ Ready ([Inspect](https://vercel.com/alchemy-dot-com/aa-sdk-ui-demo/F9dn7BqqGwnhZCPLuV6ynhyPwPwe)) | [Visit Preview](https://aa-sdk-ui-demo-git-zer0dot-permission-bu-217abb-alchemy-dot-com.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/aa-sdk-ui-demo-git-zer0dot-permission-bu-217abb-alchemy-dot-com.vercel.app?via=pr-comment-feedback-link) | Apr 9, 2025 0:47am |



author_association: NONE
user: login: graphite-app[bot]
created_at: 2025-04-08T20:52:33Z
body: ## How to use the Graphite Merge Queue

Add the label <kbd>graphite-merge-queue</kbd> to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/alchemyplatform?ref=merge-queue-instructions-comment&prId=6076792949).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-08T20:54:06Z
body: ## 🌿 Documentation Preview

| Name | Status | Preview | Updated (UTC) |
| :--- | :------ | :------ | :------ |
| **Alchemy Docs** | ✅ Ready | [🔗 Visit Preview](https://alchemy-preview-47b26bbf-84f4-405f-9d9a-10fa1f2eb2a4.docs.buildwithfern.com) | Apr 9, 2025, 12:31 AM |

>
author_association: CONTRIBUTOR