diff --git a/packages/delegator-e2e/test/actions/erc7710sendTransactionWithDelegation.test.ts b/packages/delegator-e2e/test/actions/erc7710sendTransactionWithDelegation.test.ts index afcf7ff0..fe29831d 100644 --- a/packages/delegator-e2e/test/actions/erc7710sendTransactionWithDelegation.test.ts +++ b/packages/delegator-e2e/test/actions/erc7710sendTransactionWithDelegation.test.ts @@ -38,7 +38,7 @@ let aliceSmartAccount: MetaMaskSmartAccount; let bob: Account; let bobPrivateKey: Hex; let aliceCounterContractAddress: Address; -let permissionsContext: Hex; +let permissionContext: Hex; let signedDelegation: Delegation; beforeEach(async () => { @@ -75,7 +75,7 @@ beforeEach(async () => { signature: await aliceSmartAccount.signDelegation({ delegation }), }; - permissionsContext = encodeDelegations([signedDelegation]); + permissionContext = encodeDelegations([signedDelegation]); }); /* @@ -113,7 +113,7 @@ test('maincase: Bob redeems the delegation in order to increment() on the counte abi: CounterMetadata.abi, functionName: 'increment', }), - permissionsContext, + permissionContext, delegationManager, }); @@ -183,7 +183,7 @@ test('Bob redelegates to Carol, who redeems the delegation to call increment() o abi: CounterMetadata.abi, functionName: 'increment', }), - permissionsContext: redelegatedPermissionsContext, + permissionContext: redelegatedPermissionsContext, delegationManager, }, ); @@ -253,7 +253,7 @@ test('Bob sends a native value transaction with delegation', async () => { signature: await aliceSmartAccount.signDelegation({ delegation }), }; - const permissionsContext = encodeDelegations([signedDelegation]); + const permissionContext = encodeDelegations([signedDelegation]); const bobWalletClient = createWalletClient({ account: bob, @@ -268,7 +268,7 @@ test('Bob sends a native value transaction with delegation', async () => { chain, to: recipient, value: maxAmount, - permissionsContext, + permissionContext, delegationManager, }); diff --git a/packages/delegator-e2e/test/actions/erc7710sendUserOperationWithDelegation.test.ts b/packages/delegator-e2e/test/actions/erc7710sendUserOperationWithDelegation.test.ts index 5f70e75a..f4bd5e57 100644 --- a/packages/delegator-e2e/test/actions/erc7710sendUserOperationWithDelegation.test.ts +++ b/packages/delegator-e2e/test/actions/erc7710sendUserOperationWithDelegation.test.ts @@ -34,7 +34,7 @@ import { expectUserOperationToSucceed } from '../utils/assertions'; let aliceSmartAccount: MetaMaskSmartAccount; let bobSmartAccount: MetaMaskSmartAccount; let aliceCounterContractAddress: Address; -let permissionsContext: Hex; +let permissionContext: Hex; let signedDelegation: Delegation; const bundlerClient = sponsoredBundlerClient.extend(erc7710BundlerActions()); @@ -81,7 +81,7 @@ beforeEach(async () => { signature: await aliceSmartAccount.signDelegation({ delegation }), }; - permissionsContext = encodeDelegations([signedDelegation]); + permissionContext = encodeDelegations([signedDelegation]); }); /* @@ -114,7 +114,7 @@ test('maincase: Bob redeems the delegation in order to call increment() on the c functionName: 'increment', }), value: 0n, - permissionsContext, + permissionContext, delegationManager: aliceSmartAccount.environment.DelegationManager, }, ], @@ -158,7 +158,7 @@ test('Bob redeems the delegation in order to call increment() on the counter con functionName: 'increment', }), value: 0n, - permissionsContext, + permissionContext, delegationManager: aliceSmartAccount.environment.DelegationManager, }, { @@ -217,7 +217,7 @@ test('Bob redeems the delegation, and deploys Alices smart account via accountMe functionName: 'increment', }), value: 0n, - permissionsContext, + permissionContext, delegationManager: aliceSmartAccount.environment.DelegationManager, }, ], @@ -268,7 +268,7 @@ test('Bob redeems the delegation, with account metadata, even though Alices acco functionName: 'increment', }), value: 0n, - permissionsContext, + permissionContext, delegationManager: aliceSmartAccount.environment.DelegationManager, }, ], diff --git a/packages/delegator-e2e/test/caveats/nativeTokenPayment.test.ts b/packages/delegator-e2e/test/caveats/nativeTokenPayment.test.ts index 7427ccc2..c4808185 100644 --- a/packages/delegator-e2e/test/caveats/nativeTokenPayment.test.ts +++ b/packages/delegator-e2e/test/caveats/nativeTokenPayment.test.ts @@ -120,11 +120,11 @@ test('maincase: Bob redeems the delegation with a permissions context allowing p }), }; - const permissionsContext = encodeDelegations([signedPaymentDelegation]); + const permissionContext = encodeDelegations([signedPaymentDelegation]); await runTest_expectSuccess( delegationRequiringNativeTokenPayment, - permissionsContext, + permissionContext, recipient, requiredValue, ); @@ -167,11 +167,11 @@ test('Bob attempts to redeem the delegation without an argsEqualityCheckEnforcer }), }; - const permissionsContext = encodeDelegations([signedPaymentDelegation]); + const permissionContext = encodeDelegations([signedPaymentDelegation]); await runTest_expectFailure( delegationRequiringNativeTokenPayment, - permissionsContext, + permissionContext, recipient, 'NativeTokenPaymentEnforcer:missing-argsEqualityCheckEnforcer', ); @@ -195,11 +195,11 @@ test('Bob attempts to redeem the delegation without providing a valid permission signature: '0x', }; - const permissionsContext = '0x' as const; + const permissionContext = '0x' as const; await runTest_expectFailure( delegationRequiringNativeTokenPayment, - permissionsContext, + permissionContext, recipient, undefined, // The NativeTokenPaymentEnforcer rejects when it fails to decode the permissions context ); @@ -256,11 +256,11 @@ test('Bob attempts to redeem with invalid terms length', async () => { }), }; - const permissionsContext = encodeDelegations([signedPaymentDelegation]); + const permissionContext = encodeDelegations([signedPaymentDelegation]); await runTest_expectFailure( delegationRequiringNativeTokenPayment, - permissionsContext, + permissionContext, recipient, 'NativeTokenPaymentEnforcer:invalid-terms-length', ); @@ -286,11 +286,11 @@ test('Bob attempts to redeem with empty allowance delegations', async () => { }; // Create empty allowance delegations array - const permissionsContext = encodeDelegations([]); + const permissionContext = encodeDelegations([]); await runTest_expectFailure( delegationRequiringNativeTokenPayment, - permissionsContext, + permissionContext, recipient, 'NativeTokenPaymentEnforcer:invalid-allowance-delegations-length', ); @@ -298,7 +298,7 @@ test('Bob attempts to redeem with empty allowance delegations', async () => { const runTest_expectSuccess = async ( delegation: Delegation, - permissionsContext: Hex, + permissionContext: Hex, recipient: Address, requiredValue: bigint, ) => { @@ -306,7 +306,7 @@ const runTest_expectSuccess = async ( address: recipient, }); - const userOpHash = await submitUserOpForTest(delegation, permissionsContext); + const userOpHash = await submitUserOpForTest(delegation, permissionContext); const receipt = await sponsoredBundlerClient.waitForUserOperationReceipt({ hash: userOpHash, @@ -326,7 +326,7 @@ const runTest_expectSuccess = async ( const runTest_expectFailure = async ( delegation: Delegation, - permissionsContext: Hex, + permissionContext: Hex, recipient: Address, expectedError: string | undefined, ) => { @@ -335,7 +335,7 @@ const runTest_expectFailure = async ( }); const rejects = expect( - submitUserOpForTest(delegation, permissionsContext), + submitUserOpForTest(delegation, permissionContext), ).rejects; if (expectedError) { @@ -355,7 +355,7 @@ const runTest_expectFailure = async ( const submitUserOpForTest = async ( delegation: Delegation, - permissionsContext: Hex, + permissionContext: Hex, ) => { const signedDelegation = { ...delegation, @@ -364,7 +364,7 @@ const submitUserOpForTest = async ( // we need to assign the permissions context to the caveat in order for it to process the payment // here we assume that the first caveat is the nativeTokenPayment caveat - signedDelegation.caveats[0].args = permissionsContext; + signedDelegation.caveats[0].args = permissionContext; const execution = createExecution({ target: zeroAddress, diff --git a/packages/smart-accounts-kit/CHANGELOG.md b/packages/smart-accounts-kit/CHANGELOG.md index 3eee26e8..5f780872 100644 --- a/packages/smart-accounts-kit/CHANGELOG.md +++ b/packages/smart-accounts-kit/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Introduce `PermissionContext` to represent a delegation chain (ABI-encoded `Hex` or decoded `Delegation[]`). ([#140](https://github.com/MetaMask/smart-accounts-kit/pull/140)) + - **Breaking**: Replace usages of raw `Hex` _or_ `Delegation[]` with `PermissionContext`, and rename `permissionsContext` to `permissionContext` (note the singular "permission") where applicable: + - `SendTransactionWithDelegation`: `permissionsContext: Hex` → `permissionContext: PermissionContext` + - `SendUserOperationWithDelegation`: within `calls: DelegatedCall`, `permissionsContext: Hex` → `permissionContext: PermissionContext` + - `redeemDelegations`: parameter `Delegation[]` → `PermissionContext` + - `encodeDelegations` and `decodeDelegations` now accept `PermissionContext` (if the input is already the expected type, the input is returned) + - `encode`, `execute`, and `simulate` functions for `DelegationManager.redeemDelegations` from `@metamask/smart-accounts-kit/contracts`: parameter `delegations: Delegation[]` → `delegations: PermissionContext` + ### Fixed - Allow scope type to be specified either as `ScopeType` enum, or string literal ([#133](https://github.com/MetaMask/smart-accounts-kit/pull/133)) diff --git a/packages/smart-accounts-kit/src/DelegationFramework/DelegationManager/methods/redeemDelegations.ts b/packages/smart-accounts-kit/src/DelegationFramework/DelegationManager/methods/redeemDelegations.ts index bbc94e1a..6d48145e 100644 --- a/packages/smart-accounts-kit/src/DelegationFramework/DelegationManager/methods/redeemDelegations.ts +++ b/packages/smart-accounts-kit/src/DelegationFramework/DelegationManager/methods/redeemDelegations.ts @@ -3,14 +3,14 @@ import type { Address, Client } from 'viem'; import { encodeFunctionData } from 'viem'; import { simulateContract, writeContract } from 'viem/actions'; -import { encodePermissionContexts } from '../../../delegation'; +import { encodeDelegations } from '../../../delegation'; import { encodeExecutionCalldatas } from '../../../executions'; import type { ExecutionMode, ExecutionStruct } from '../../../executions'; -import type { Delegation } from '../../../types'; +import type { PermissionContext } from '../../../types'; import type { InitializedClient } from '../../types'; export type EncodeRedeemDelegationsParameters = { - delegations: Delegation[][]; + delegations: PermissionContext[]; modes: ExecutionMode[]; executions: ExecutionStruct[][]; }; @@ -37,7 +37,7 @@ export const simulate = async ({ abi: DelegationManager, functionName: 'redeemDelegations', args: [ - encodePermissionContexts(delegations), + delegations.map((delegation) => encodeDelegations(delegation)), modes, encodeExecutionCalldatas(executions), ], @@ -71,7 +71,7 @@ export const encode = ({ abi: DelegationManager, functionName: 'redeemDelegations', args: [ - encodePermissionContexts(delegations), + delegations.map((delegation) => encodeDelegations(delegation)), modes, encodeExecutionCalldatas(executions), ], diff --git a/packages/smart-accounts-kit/src/actions/erc7710RedeemDelegationAction.ts b/packages/smart-accounts-kit/src/actions/erc7710RedeemDelegationAction.ts index 6074c706..1a98da8e 100644 --- a/packages/smart-accounts-kit/src/actions/erc7710RedeemDelegationAction.ts +++ b/packages/smart-accounts-kit/src/actions/erc7710RedeemDelegationAction.ts @@ -17,16 +17,19 @@ import type { SmartAccount, } from 'viem/account-abstraction'; +import { encodeDelegations } from '../delegation'; import { createExecution, encodeExecutionCalldatas, ExecutionMode, } from '../executions'; import { getSmartAccountsEnvironment } from '../smartAccountsEnvironment'; -import type { Call } from '../types'; +import type { Call, PermissionContext } from '../types'; export type DelegatedCall = Call & - OneOf<{ permissionsContext: Hex; delegationManager: Hex } | object>; + OneOf< + { permissionContext: PermissionContext; delegationManager: Hex } | object + >; export type SendTransactionWithDelegationParameters< TChain extends Chain | undefined = Chain | undefined, @@ -35,7 +38,7 @@ export type SendTransactionWithDelegationParameters< TRequest extends SendTransactionRequest = SendTransactionRequest, > = SendTransactionParameters & { - permissionsContext: Hex; + permissionContext: PermissionContext; delegationManager: Hex; }; @@ -71,7 +74,7 @@ export async function sendTransactionWithDelegationAction< abi: DelegationManager, functionName: 'redeemDelegations', args: [ - [args.permissionsContext], + [encodeDelegations(args.permissionContext)], [ExecutionMode.SingleDefault], encodeExecutionCalldatas([executions]), ], @@ -79,7 +82,7 @@ export async function sendTransactionWithDelegationAction< const { value: _value, - permissionsContext: _permissionsContext, + permissionContext: _permissionContext, delegationManager: _delegationManager, ...rest } = args; @@ -127,7 +130,7 @@ export type SendUserOperationWithDelegationParameters< * functionName: 'increment', * }), * value: 0n, - * permissionsContext: '0x...', + * permissionContext: '0x...', * delegationManager: '0x...', * }, * ], @@ -206,6 +209,22 @@ export async function sendUserOperationWithDelegationAction< ]; } + parameters.calls = parameters.calls.map((call) => { + if (!('permissionContext' in call)) { + return call; + } + + const { permissionContext } = call; + if (!permissionContext) { + return call; + } + + return { + ...call, + permissionContext: encodeDelegations(permissionContext), + }; + }); + return client.sendUserOperation( parameters as unknown as SendUserOperationParameters, ); diff --git a/packages/smart-accounts-kit/src/caveats.ts b/packages/smart-accounts-kit/src/caveats.ts index 73821ce6..0c2c9f57 100644 --- a/packages/smart-accounts-kit/src/caveats.ts +++ b/packages/smart-accounts-kit/src/caveats.ts @@ -1,6 +1,5 @@ import { type Hex, - encodePacked, encodeAbiParameters, parseAbiParameters, keccak256, @@ -33,22 +32,6 @@ export const getCaveatPacketHash = (input: Caveat): Hex => { return keccak256(encoded); }; -/** - * Calculates the hash of an array of Caveats. - * - * @param input - The array of Caveats. - * @returns The keccak256 hash of the encoded Caveat array packet. - */ -export const getCaveatArrayPacketHash = (input: Caveat[]): Hex => { - let encoded: Hex = '0x'; - - for (const caveat of input) { - const caveatPacketHash = getCaveatPacketHash(caveat); - encoded = encodePacked(['bytes', 'bytes32'], [encoded, caveatPacketHash]); - } - return keccak256(encoded); -}; - /** * Creates a caveat. * diff --git a/packages/smart-accounts-kit/src/delegation.ts b/packages/smart-accounts-kit/src/delegation.ts index b8e86d45..b7d71e17 100644 --- a/packages/smart-accounts-kit/src/delegation.ts +++ b/packages/smart-accounts-kit/src/delegation.ts @@ -7,14 +7,18 @@ import { CAVEAT_TYPEHASH, ROOT_AUTHORITY, } from '@metamask/delegation-core'; -import { hashMessage, toBytes, toHex, getAddress } from 'viem'; +import { toHex, getAddress } from 'viem'; import type { TypedData, AbiParameter, Address, Hex } from 'viem'; import { signTypedData } from 'viem/accounts'; import { type Caveats, resolveCaveats } from './caveatBuilder'; import type { ScopeConfig } from './caveatBuilder/scope'; import { CAVEAT_ABI_TYPE_COMPONENTS } from './caveats'; -import type { Delegation, SmartAccountsEnvironment } from './types'; +import type { + Delegation, + PermissionContext, + SmartAccountsEnvironment, +} from './types'; export { ANY_BENEFICIARY, @@ -93,13 +97,16 @@ export type DelegationStruct = Omit & { /** * ABI Encodes an array of delegations. * - * @param delegations - The delegations to encode. + * @param delegations - The delegations to encode, either as an array of delegations or the ABI encoding of the array of delegations. * @returns The encoded delegations. */ -export const encodeDelegations = (delegations: Delegation[]): Hex => { - const delegationStructs = delegations.map(toDelegationStruct); +export const encodeDelegations = (delegations: PermissionContext): Hex => { + if (Array.isArray(delegations)) { + const delegationStructs = delegations.map(toDelegationStruct); - return encodeDelegationsCore(delegationStructs); + return encodeDelegationsCore(delegationStructs); + } + return delegations; }; /** @@ -119,12 +126,17 @@ export const encodePermissionContexts = (delegations: Delegation[][]) => { /** * Decodes an array of delegations from its ABI-encoded representation. * - * @param encoded - The hex-encoded delegation array to decode. + * @param delegations - The delegations to decode, either as an array of delegations or its ABI-encoded hex representation. * @returns An array of decoded delegations. */ -export const decodeDelegations = (encoded: Hex): Delegation[] => { +export const decodeDelegations = ( + delegations: PermissionContext, +): Delegation[] => { + if (Array.isArray(delegations)) { + return delegations; + } // decodeDelegationsCore returns DelegationStruct, so we need to map it back to Delegation - return decodeDelegationsCore(encoded).map(toDelegation); + return decodeDelegationsCore(delegations).map(toDelegation); }; /** @@ -164,18 +176,6 @@ export const DELEGATION_ARRAY_ABI_TYPE: AbiParameter = { components: DELEGATION_ABI_TYPE_COMPONENTS, } as const; -/** - * Prepares a delegation hash for passkey signing. - * - * @param delegationHash - The delegation hash to prepare. - * @returns The prepared hash for passkey signing. - */ -export const prepDelegationHashForPasskeySign = (delegationHash: Hex) => { - return hashMessage({ - raw: toBytes(delegationHash), - }); -}; - /** * Gets a delegation hash offchain. * diff --git a/packages/smart-accounts-kit/src/encodeCalls.ts b/packages/smart-accounts-kit/src/encodeCalls.ts index 2bc46b17..47afff94 100644 --- a/packages/smart-accounts-kit/src/encodeCalls.ts +++ b/packages/smart-accounts-kit/src/encodeCalls.ts @@ -3,6 +3,7 @@ import { encodeFunctionData } from 'viem'; import type { Address, Hex } from 'viem'; import type { DelegatedCall } from './actions/erc7710RedeemDelegationAction'; +import { encodeDelegations } from './delegation'; import { execute, executeWithMode, @@ -15,17 +16,17 @@ import { import type { Call } from './types'; /** - * Checks if a call is a delegated call by checking for the presence of permissionsContext and delegationManager. + * Checks if a call is a delegated call by checking for the presence of permissionContext and delegationManager. * * @param call - The call to check. * @returns True if the call is a delegated call, false otherwise. */ const isDelegatedCall = (call: Call): call is DelegatedCall => { - return 'permissionsContext' in call && 'delegationManager' in call; + return 'permissionContext' in call && 'delegationManager' in call; }; /** - * If there's a single call with permissionsContext and delegationManager, + * If there's a single call with permissionContext and delegationManager, * processes it as a delegated call. * * @param call - The call to process. @@ -35,7 +36,7 @@ const isDelegatedCall = (call: Call): call is DelegatedCall => { */ const processDelegatedCall = (call: DelegatedCall) => { const { - permissionsContext, + permissionContext, delegationManager, to: target, value, @@ -44,15 +45,17 @@ const processDelegatedCall = (call: DelegatedCall) => { const callAsExecution = createExecution({ target, value, callData }); - if (!permissionsContext) { + if (!permissionContext) { return callAsExecution; } + const encodedPermissionsContext = encodeDelegations(permissionContext); + const redeemCalldata = encodeFunctionData({ abi: DelegationManager, functionName: 'redeemDelegations', args: [ - [permissionsContext], + [encodedPermissionsContext], [ExecutionMode.SingleDefault], encodeExecutionCalldatas([[callAsExecution]]), ], diff --git a/packages/smart-accounts-kit/src/index.ts b/packages/smart-accounts-kit/src/index.ts index 0c4dee8b..88186c3b 100644 --- a/packages/smart-accounts-kit/src/index.ts +++ b/packages/smart-accounts-kit/src/index.ts @@ -29,6 +29,7 @@ export type { MultiSigSignerConfig, Delegation, Caveat, + PermissionContext, } from './types'; export { diff --git a/packages/smart-accounts-kit/src/types.ts b/packages/smart-accounts-kit/src/types.ts index e29ebe93..62a4302e 100644 --- a/packages/smart-accounts-kit/src/types.ts +++ b/packages/smart-accounts-kit/src/types.ts @@ -64,6 +64,16 @@ export type Delegation = { signature: Hex; }; +/** + * A permission context describing delegation authority. + * + * Either a decoded delegation chain (`Delegation[]`) or its ABI-encoded hex + * representation. When multiple delegations are provided, each delegation's + * `authority` should reference the hash of the delegation it inherits from, + * and the list should be ordered from leaf (most recent delegate) to root. + */ +export type PermissionContext = Delegation[] | Hex; + /** * A version agnostic blob of contract addresses required for the DeleGator system to function. */ @@ -136,7 +146,7 @@ export type PackedUserOperationStruct = { * Redemption data, including delegations, executions, and mode. */ export type Redemption = { - permissionContext: Delegation[]; + permissionContext: PermissionContext; executions: ExecutionStruct[]; mode: ExecutionMode; }; diff --git a/packages/smart-accounts-kit/src/write.ts b/packages/smart-accounts-kit/src/write.ts index 220ae897..cba39c7a 100644 --- a/packages/smart-accounts-kit/src/write.ts +++ b/packages/smart-accounts-kit/src/write.ts @@ -1,38 +1,11 @@ -import { SimpleFactory, DelegationManager } from '@metamask/delegation-abis'; -import type { Address, Chain, Hex, PublicClient, WalletClient } from 'viem'; +import { DelegationManager } from '@metamask/delegation-abis'; +import type { Address, Chain, PublicClient, WalletClient } from 'viem'; -import { encodePermissionContexts } from './delegation'; +import { decodeDelegations, encodePermissionContexts } from './delegation'; import type { ExecutionStruct, ExecutionMode } from './executions'; import { encodeExecutionCalldatas } from './executions'; import type { Delegation, ContractMetaData, Redemption } from './types'; -/** - * Deploys a contract using the SimpleFactory contract. - * - * @param walletClient - The wallet client to use for deployment. - * @param publicClient - The public client to use for simulation. - * @param simpleFactoryAddress - The address of the SimpleFactory contract. - * @param creationCode - The creation code for the contract to deploy. - * @param salt - The salt to use for deterministic deployment. - * @returns The transaction hash of the deployment. - */ -export const deployWithSimpleFactory = async ( - walletClient: WalletClient, - publicClient: PublicClient, - simpleFactoryAddress: Address, - creationCode: Hex, - salt: Hex, -) => { - const { request } = await publicClient.simulateContract({ - account: walletClient.account, - address: simpleFactoryAddress, - abi: SimpleFactory, - functionName: 'deploy', - args: [creationCode, salt], - }); - return await walletClient.writeContract(request); -}; - /** * Redeems a delegation to execute the provided executions. * @@ -57,7 +30,11 @@ export const redeemDelegations = async ( const executionModes: ExecutionMode[] = []; redemptions.forEach((redemption) => { - permissionContexts.push(redemption.permissionContext); + const decodedPermissionContext = decodeDelegations( + redemption.permissionContext, + ); + + permissionContexts.push(decodedPermissionContext); executionsBatch.push(redemption.executions); executionModes.push(redemption.mode); }); diff --git a/packages/smart-accounts-kit/test/DelegationFramework/DelegationManager/delegationManagement.test.ts b/packages/smart-accounts-kit/test/DelegationFramework/DelegationManager/delegationManagement.test.ts index 338d9db5..28c5b2a7 100644 --- a/packages/smart-accounts-kit/test/DelegationFramework/DelegationManager/delegationManagement.test.ts +++ b/packages/smart-accounts-kit/test/DelegationFramework/DelegationManager/delegationManagement.test.ts @@ -2,7 +2,7 @@ import { privateKeyToAccount } from 'viem/accounts'; import { describe, expect, it } from 'vitest'; import { ScopeType } from '../../../src/constants'; -import { createDelegation } from '../../../src/delegation'; +import { createDelegation, encodeDelegations } from '../../../src/delegation'; import * as DelegationManager from '../../../src/DelegationFramework/DelegationManager'; import { ExecutionMode, createExecution } from '../../../src/executions'; import type { SmartAccountsEnvironment } from '../../../src/types'; @@ -136,6 +136,9 @@ describe('DelegationManager - Delegation Management', () => { }); describe('redeemDelegations', () => { + const expectedEncodedData = + '0xcef6d209000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000005200000000000000000000000000000000000000000000000000000000000000560000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000460000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000bb31666e59506a3d6e165274374edd84c4a591f400000000000000000000000091de59fa0ccc6913b590228738cf6a13369c3e73ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000220000000000000000000000000aadb0309cb7fa980815a2faf3975ed03ab264e96000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001491de59fa0ccc6913b590228738cf6a13369c3e7300000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a6b516ce732c0bc173dab989626844d3c0ce533f000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000358d769e0ae28b08eb6b761f1c2e2c70fce9fe7a000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003491de59fa0ccc6913b590228738cf6a13369c3e730000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + it('should encode redeemDelegations correctly', () => { const delegation = createDelegation({ to: bob.address, @@ -158,9 +161,33 @@ describe('DelegationManager - Delegation Management', () => { executions: [[execution]], }); - expect(encodedData).toStrictEqual( - '0xcef6d209000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000005200000000000000000000000000000000000000000000000000000000000000560000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000460000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000bb31666e59506a3d6e165274374edd84c4a591f400000000000000000000000091de59fa0ccc6913b590228738cf6a13369c3e73ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000220000000000000000000000000aadb0309cb7fa980815a2faf3975ed03ab264e96000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001491de59fa0ccc6913b590228738cf6a13369c3e7300000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a6b516ce732c0bc173dab989626844d3c0ce533f000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000358d769e0ae28b08eb6b761f1c2e2c70fce9fe7a000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003491de59fa0ccc6913b590228738cf6a13369c3e730000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - ); + expect(encodedData).toStrictEqual(expectedEncodedData); + }); + + it('should encode redeemDelegations with encoded permission contexts', () => { + const delegation = createDelegation({ + to: bob.address, + from: alice.address, + environment, + scope: { + type: 'functionCall', + targets: [alice.address], + selectors: ['0x00000000'], + }, + }); + + const execution = createExecution({ + target: alice.address, + }); + + const encodedPermissionContext = encodeDelegations([delegation]); + const encodedData = DelegationManager.encode.redeemDelegations({ + delegations: [encodedPermissionContext], + modes: [ExecutionMode.SingleDefault], + executions: [[execution]], + }); + + expect(encodedData).toStrictEqual(expectedEncodedData); }); }); }); diff --git a/packages/smart-accounts-kit/test/actions/erc7710RedeemDelegationAction.test.ts b/packages/smart-accounts-kit/test/actions/erc7710RedeemDelegationAction.test.ts index cc4242d6..9584cba0 100644 --- a/packages/smart-accounts-kit/test/actions/erc7710RedeemDelegationAction.test.ts +++ b/packages/smart-accounts-kit/test/actions/erc7710RedeemDelegationAction.test.ts @@ -24,6 +24,7 @@ import type { SendUserOperationWithDelegationParameters, } from '../../src/actions/erc7710RedeemDelegationAction'; import { Implementation } from '../../src/constants'; +import { encodeDelegations } from '../../src/delegation'; import { createExecution, encodeExecutionCalldatas, @@ -32,12 +33,22 @@ import { import { overrideDeployedEnvironment } from '../../src/smartAccountsEnvironment'; import { toMetaMaskSmartAccount } from '../../src/toMetaMaskSmartAccount'; import type { + Delegation, SmartAccountsEnvironment, MetaMaskSmartAccount, } from '../../src/types'; import { randomAddress, randomBytes } from '../utils'; describe('erc7710RedeemDelegationAction', () => { + const createDelegation = (): Delegation => ({ + delegate: randomAddress(), + delegator: randomAddress(), + authority: randomBytes(32), + caveats: [], + salt: randomBytes(32), + signature: randomBytes(65), + }); + describe('sendUserOperationWithDelegationAction()', () => { const mockBundlerRequest = stub(); let publicClient: PublicClient; @@ -154,6 +165,101 @@ describe('erc7710RedeemDelegationAction', () => { }); }); + it('should encode delegation arrays in permissionContext calls', async () => { + const bundlerClient = createBundlerClient({ + transport: custom({ request: mockBundlerRequest }), + chain, + }); + const extendedBundlerClient = bundlerClient.extend( + erc7710BundlerActions(), + ); + + const sendUserOperationStub = stub(bundlerClient, 'sendUserOperation'); + const delegationChain = [createDelegation()]; + const permissionContext = encodeDelegations(delegationChain); + + const sendUserOperationWithDelegationArgs: SendUserOperationWithDelegationParameters = + { + publicClient, + calls: [ + { + to: randomAddress(), + value: 0n, + permissionContext: delegationChain, + delegationManager: randomAddress(), + }, + ], + }; + + await extendedBundlerClient.sendUserOperationWithDelegation( + sendUserOperationWithDelegationArgs, + ); + + expect(sendUserOperationStub.firstCall.args[0]).to.deep.equal({ + ...sendUserOperationWithDelegationArgs, + calls: [ + { + ...sendUserOperationWithDelegationArgs.calls[0], + permissionContext, + }, + ], + }); + }); + + it('should preserve mixed calls and only encode delegation arrays', async () => { + const bundlerClient = createBundlerClient({ + transport: custom({ request: mockBundlerRequest }), + chain, + }); + const extendedBundlerClient = bundlerClient.extend( + erc7710BundlerActions(), + ); + + const sendUserOperationStub = stub(bundlerClient, 'sendUserOperation'); + const delegationChain = [createDelegation()]; + const encodedPermissionsContext = encodeDelegations(delegationChain); + const alreadyEncoded = `0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`; + + const sendUserOperationWithDelegationArgs: SendUserOperationWithDelegationParameters = + { + publicClient, + calls: [ + { + to: randomAddress(), + value: 0n, + permissionContext: delegationChain, + delegationManager: randomAddress(), + }, + { + to: randomAddress(), + value: 0n, + permissionContext: alreadyEncoded, + delegationManager: randomAddress(), + }, + { + to: randomAddress(), + value: 0n, + }, + ], + }; + + await extendedBundlerClient.sendUserOperationWithDelegation( + sendUserOperationWithDelegationArgs, + ); + + expect(sendUserOperationStub.firstCall.args[0]).to.deep.equal({ + ...sendUserOperationWithDelegationArgs, + calls: [ + { + ...sendUserOperationWithDelegationArgs.calls[0], + permissionContext: encodedPermissionsContext, + }, + sendUserOperationWithDelegationArgs.calls[1], + sendUserOperationWithDelegationArgs.calls[2], + ], + }); + }); + it('should throw an error when SimpleFactory is provided as accountMetadata factory', async () => { const bundlerClient = createBundlerClient({ transport: custom({ request: mockBundlerRequest }), @@ -283,7 +389,7 @@ describe('erc7710RedeemDelegationAction', () => { to: randomAddress(), value: 0n, data: randomBytes(128), - permissionsContext: randomBytes(128), + permissionContext: randomBytes(128), delegationManager: randomAddress(), }; @@ -293,11 +399,15 @@ describe('erc7710RedeemDelegationAction', () => { throw new Error('to is not set'); } + const encodedPermissionContext = encodeDelegations( + args.permissionContext, + ); + const redeemDelegationCallData = encodeFunctionData({ abi: DelegationManager, functionName: 'redeemDelegations', args: [ - [args.permissionsContext], + [encodedPermissionContext], [ExecutionMode.SingleDefault], encodeExecutionCalldatas([ [ @@ -319,7 +429,7 @@ describe('erc7710RedeemDelegationAction', () => { to: delegationManager, // value is not passed to sendTransaction data: redeemDelegationCallData, - // permissionsContext and delegationManager are not passed to sendTransaction + // permissionContext and delegationManager are not passed to sendTransaction }; expect(sendTransaction.calledOnce).to.equal(true); @@ -336,7 +446,7 @@ describe('erc7710RedeemDelegationAction', () => { chain, value: 0n, data: randomBytes(128), - permissionsContext: randomBytes(128), + permissionContext: randomBytes(128), delegationManager: randomAddress(), }), ).rejects.toThrow( @@ -344,7 +454,7 @@ describe('erc7710RedeemDelegationAction', () => { ); }); - it('should not encode the specified `value`, `permissionsContext` and `delegationManager` into the resulting transaction', async () => { + it('should not encode the specified `value`, `permissionContext` and `delegationManager` into the resulting transaction', async () => { const extendedWalletClient = walletClient.extend(erc7710WalletActions()); const sendTransaction = stub(walletClient, 'sendTransaction'); @@ -355,7 +465,7 @@ describe('erc7710RedeemDelegationAction', () => { to: randomAddress(), value: 100n, data: randomBytes(128), - permissionsContext: randomBytes(128), + permissionContext: randomBytes(128), delegationManager: randomAddress(), }; @@ -364,12 +474,60 @@ describe('erc7710RedeemDelegationAction', () => { expect(sendTransaction.calledOnce).to.equal(true); const sendTransactionArgs = sendTransaction.firstCall.args[0]; expect(sendTransactionArgs.value).to.equal(undefined); - expect((sendTransactionArgs as any).permissionsContext).to.equal( + expect((sendTransactionArgs as any).permissionContext).to.equal( undefined, ); expect((sendTransactionArgs as any).delegationManager).to.equal( undefined, ); }); + + it('should encode delegation arrays in permissionContext', async () => { + const extendedWalletClient = walletClient.extend(erc7710WalletActions()); + + const sendTransaction = stub(walletClient, 'sendTransaction'); + const delegationChain = [createDelegation()]; + const permissionContext = encodeDelegations(delegationChain); + + const to = randomAddress(); + + const args: SendTransactionWithDelegationParameters = { + account, + chain, + to, + value: 0n, + data: randomBytes(128), + permissionContext: delegationChain, + delegationManager: randomAddress(), + }; + + await extendedWalletClient.sendTransactionWithDelegation(args); + + const redeemDelegationCallData = encodeFunctionData({ + abi: DelegationManager, + functionName: 'redeemDelegations', + args: [ + [permissionContext], + [ExecutionMode.SingleDefault], + encodeExecutionCalldatas([ + [ + createExecution({ + target: to, + value: args.value, + callData: args.data, + }), + ], + ]), + ], + }); + + expect(sendTransaction.calledOnce).to.equal(true); + expect(sendTransaction.firstCall.args[0]).to.deep.equal({ + account, + chain, + to: args.delegationManager, + data: redeemDelegationCallData, + }); + }); }); }); diff --git a/packages/smart-accounts-kit/test/encodeCalls.test.ts b/packages/smart-accounts-kit/test/encodeCalls.test.ts index 98d66b3d..54dc1765 100644 --- a/packages/smart-accounts-kit/test/encodeCalls.test.ts +++ b/packages/smart-accounts-kit/test/encodeCalls.test.ts @@ -1,15 +1,25 @@ -import { DeleGatorCore } from '@metamask/delegation-abis'; -import type { Address } from 'viem'; +import { DeleGatorCore, DelegationManager } from '@metamask/delegation-abis'; +import type { Address, Hex } from 'viem'; import { encodeFunctionData } from 'viem'; import { describe, it, expect } from 'vitest'; +import type { DelegatedCall } from '../src/actions/erc7710RedeemDelegationAction'; +import { encodeDelegations } from '../src/delegation'; import { encodeCallsForCaller } from '../src/encodeCalls'; import { ExecutionMode, encodeExecutionCalldatas } from '../src/executions'; import type { ExecutionStruct } from '../src/executions'; -import { type Call } from '../src/types'; +import { type Call, type Delegation } from '../src/types'; describe('encodeCallsForCaller', () => { const caller: Address = '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC'; + const delegation: Delegation = { + delegate: '0x1111111111111111111111111111111111111111', + delegator: '0x2222222222222222222222222222222222222222', + authority: `0x1111111111111111111111111111111111111111111111111111111111111111`, + caveats: [], + salt: `0x${'22'.repeat(32)}`, + signature: '0x', + }; it('should return the call data directly for a single call to the delegator', async () => { const calls: Call[] = [ @@ -190,4 +200,183 @@ describe('encodeCallsForCaller', () => { expect(encodedCalls).to.equal(expectedCalldata); }); + + it('should encode delegated calls with an encoded permissionContext', async () => { + const permissionContext = `0x3333333333333333333333333333333333333333`; + const encodedPermissionContext = encodeDelegations(permissionContext); + const delegationManager: Address = + '0x3333333333333333333333333333333333333333'; + const target: Address = '0x4444444444444444444444444444444444444444'; + + const calls: DelegatedCall[] = [ + { + to: target, + data: '0xabcdef', + value: 100n, + permissionContext, + delegationManager, + }, + ]; + + const encodedCalls = await encodeCallsForCaller(caller, calls); + const redemptionCalldata = encodeFunctionData({ + abi: DelegationManager, + functionName: 'redeemDelegations', + args: [ + [encodedPermissionContext], + [ExecutionMode.SingleDefault], + encodeExecutionCalldatas([ + [ + { + target, + value: 100n, + callData: '0xabcdef', + }, + ], + ]), + ], + }); + + const expectedExecutionCalldatas = encodeExecutionCalldatas([ + [ + { + target: delegationManager, + value: 0n, + callData: redemptionCalldata, + }, + ], + ]) as [Hex]; + + const expectedEncodedCalls = encodeFunctionData({ + abi: DeleGatorCore, + functionName: 'execute', + args: [ExecutionMode.SingleDefault, expectedExecutionCalldatas[0]], + }); + + expect(encodedCalls).to.equal(expectedEncodedCalls); + }); + + it('should encode delegated calls with delegation arrays', async () => { + const delegationManager: Address = + '0x3333333333333333333333333333333333333333'; + const target: Address = '0x4444444444444444444444444444444444444444'; + const permissionContext = encodeDelegations([delegation]); + + const calls: DelegatedCall[] = [ + { + to: target, + data: '0xabcdef', + value: 100n, + permissionContext: [delegation], + delegationManager, + }, + ]; + + const encodedCalls = await encodeCallsForCaller(caller, calls); + const redemptionCalldata = encodeFunctionData({ + abi: DelegationManager, + functionName: 'redeemDelegations', + args: [ + [permissionContext], + [ExecutionMode.SingleDefault], + encodeExecutionCalldatas([ + [ + { + target, + value: 100n, + callData: '0xabcdef', + }, + ], + ]), + ], + }); + + const expectedExecutionCalldatas = encodeExecutionCalldatas([ + [ + { + target: delegationManager, + value: 0n, + callData: redemptionCalldata, + }, + ], + ]) as [Hex]; + + const expectedEncodedCalls = encodeFunctionData({ + abi: DeleGatorCore, + functionName: 'execute', + args: [ExecutionMode.SingleDefault, expectedExecutionCalldatas[0]], + }); + + expect(encodedCalls).to.equal(expectedEncodedCalls); + }); + + it('should encode mixed calls with a delegation array in batch execution', async () => { + const delegationManager: Address = + '0x3333333333333333333333333333333333333333'; + const target: Address = '0x4444444444444444444444444444444444444444'; + const otherTarget: Address = '0x5555555555555555555555555555555555555555'; + const permissionContext = encodeDelegations([delegation]); + + const calls: DelegatedCall[] = [ + { + to: target, + data: '0xabcdef', + value: 100n, + permissionContext: [delegation], + delegationManager, + }, + { + to: otherTarget, + data: '0x1234', + value: 0n, + }, + ]; + + const encodedCalls = await encodeCallsForCaller(caller, calls); + const redemptionCalldata = encodeFunctionData({ + abi: DelegationManager, + functionName: 'redeemDelegations', + args: [ + [permissionContext], + [ExecutionMode.SingleDefault], + encodeExecutionCalldatas([ + [ + { + target, + value: 100n, + callData: '0xabcdef', + }, + ], + ]), + ], + }); + + const expectedExecutions: ExecutionStruct[] = [ + { + target: delegationManager, + value: 0n, + callData: redemptionCalldata, + }, + { + target: otherTarget, + value: 0n, + callData: '0x1234', + }, + ]; + + const expectedExecutionCalldata = encodeExecutionCalldatas([ + expectedExecutions, + ])[0]; + if (!expectedExecutionCalldata) { + throw new Error('expectedExecutionCalldata is not set'); + } + + const expectedEncodedCalls = encodeFunctionData({ + abi: DeleGatorCore, + functionName: 'execute', + args: [ExecutionMode.BatchDefault, expectedExecutionCalldata], + }); + + expect(encodedCalls).to.equal(expectedEncodedCalls); + }); });