diff --git a/packages/test/v0.7/permissionValidator.test.ts b/packages/test/v0.7/permissionValidator.test.ts index 335b02e4..fe4679ba 100644 --- a/packages/test/v0.7/permissionValidator.test.ts +++ b/packages/test/v0.7/permissionValidator.test.ts @@ -25,6 +25,9 @@ import { getAbiItem, hashMessage, hashTypedData, + hexToBytes, + pad, + parseAbi, parseEther, toFunctionSelector, zeroAddress @@ -1229,6 +1232,353 @@ describe("Permission kernel Account", () => { TEST_TIMEOUT ) + test( + "Smart account client send transaction with CallPolicy V0.0.5", + async () => { + const callPolicy = toCallPolicy({ + policyVersion: CallPolicyVersion.V0_0_5, + permissions: [ + { + abi: TEST_ERC20Abi, + target: Test_ERC20Address, + functionName: "transfer", + args: [ + { + condition: ParamCondition.EQUAL, + value: owner.address + }, + null + ] + } + ] + }) + + const permissionSmartAccountClient = await getKernelAccountClient({ + account: await getSignerToPermissionKernelAccount([callPolicy]), + paymaster: zeroDevPaymaster + }) + + await mintToAccount( + permissionSmartAccountClient.account.address, + 100000000n + ) + + const amountToTransfer = 10000n + const transferData = encodeFunctionData({ + abi: TEST_ERC20Abi, + functionName: "transfer", + args: [owner.address, amountToTransfer] + }) + + const balanceOfReceipientBefore = await publicClient.readContract({ + abi: TEST_ERC20Abi, + address: Test_ERC20Address, + functionName: "balanceOf", + args: [owner.address] + }) + + console.log("balanceOfReceipientBefore", balanceOfReceipientBefore) + + const response = await permissionSmartAccountClient.sendTransaction( + { + to: Test_ERC20Address, + data: transferData + } + ) + + console.log("Transaction hash:", response) + + const balanceOfReceipientAfter = await publicClient.readContract({ + abi: TEST_ERC20Abi, + address: Test_ERC20Address, + functionName: "balanceOf", + args: [owner.address] + }) + + console.log("balanceOfReceipientAfter", balanceOfReceipientAfter) + + expect(balanceOfReceipientAfter).toBe( + balanceOfReceipientBefore + amountToTransfer + ) + }, + TEST_TIMEOUT + ) + + test( + "Smart account client send transaction with SLICE_EQUAL condition with bytes", + async () => { + const testAbi = parseAbi(["function test(bytes data) public"]) + + const callPolicy = toCallPolicy({ + policyVersion: CallPolicyVersion.V0_0_5, + permissions: [ + { + abi: testAbi, + target: zeroAddress, + functionName: "test", + args: [ + { + condition: ParamCondition.SLICE_EQUAL, + value: "0xff", + start: 1, + length: 1 + } + ] + } + ] + }) + + const permissionSmartAccountClient = await getKernelAccountClient({ + account: await getSignerToPermissionKernelAccount([callPolicy]), + paymaster: zeroDevPaymaster + }) + + const callData = encodeFunctionData({ + abi: testAbi, + functionName: "test", + args: ["0x00ff"] + }) + + const response = await permissionSmartAccountClient.sendTransaction( + { + to: zeroAddress, + data: callData + } + ) + + console.log("Transaction hash:", response) + }, + TEST_TIMEOUT + ) + + test( + "Smart account client send transaction with SLICE_EQUAL condition with string", + async () => { + const testAbi = parseAbi([ + "function test(string calldata data) public" + ]) + + const callPolicy = toCallPolicy({ + policyVersion: CallPolicyVersion.V0_0_5, + permissions: [ + { + abi: testAbi, + target: zeroAddress, + functionName: "test", + args: [ + { + condition: ParamCondition.SLICE_EQUAL, + value: "kernel", + start: 2, + length: 6 + } + ] + } + ] + }) + + const permissionSmartAccountClient = await getKernelAccountClient({ + account: await getSignerToPermissionKernelAccount([callPolicy]), + paymaster: zeroDevPaymaster + }) + + const callData = encodeFunctionData({ + abi: testAbi, + functionName: "test", + args: ["0xkernel"] + }) + + const response = await permissionSmartAccountClient.sendTransaction( + { + to: zeroAddress, + data: callData + } + ) + + console.log("Transaction hash:", response) + }, + TEST_TIMEOUT + ) + + test( + "Smart account client send transaction with SLICE_EQUAL condition with hex string", + async () => { + const testAbi = parseAbi([ + "function test(string calldata data) public" + ]) + + const callPolicy = toCallPolicy({ + policyVersion: CallPolicyVersion.V0_0_5, + permissions: [ + { + abi: testAbi, + target: zeroAddress, + functionName: "test", + args: [ + { + condition: ParamCondition.SLICE_EQUAL, + value: "0xffff", + start: 0, + length: 6 + } + ] + } + ] + }) + + const permissionSmartAccountClient = await getKernelAccountClient({ + account: await getSignerToPermissionKernelAccount([callPolicy]), + paymaster: zeroDevPaymaster + }) + + const callData = encodeFunctionData({ + abi: testAbi, + functionName: "test", + args: ["0xffff00"] + }) + + const response = await permissionSmartAccountClient.sendTransaction( + { + to: zeroAddress, + data: callData + } + ) + + console.log("Transaction hash:", response) + }, + TEST_TIMEOUT + ) + + test( + "should fail with Smart account client send transaction with SLICE_EQUAL condition", + async () => { + const testAbi = parseAbi(["function test(bytes data) public"]) + + const callPolicy = toCallPolicy({ + policyVersion: CallPolicyVersion.V0_0_5, + permissions: [ + { + abi: testAbi, + target: zeroAddress, + functionName: "test", + args: [ + { + condition: ParamCondition.SLICE_EQUAL, + value: "0xff", + start: 1, + length: 1 + } + ] + } + ] + }) + + const permissionSmartAccountClient = await getKernelAccountClient({ + account: await getSignerToPermissionKernelAccount([callPolicy]), + paymaster: zeroDevPaymaster + }) + + const invalidCallData = encodeFunctionData({ + abi: testAbi, + functionName: "test", + args: ["0x00ee"] + }) + + await expect( + permissionSmartAccountClient.sendTransaction({ + to: zeroAddress, + data: invalidCallData + }) + ).rejects.toThrow() + }, + TEST_TIMEOUT + ) + + test( + "should fail with SLICE_EQUAL condition, start and length not provided", + async () => { + const testAbi = parseAbi(["function test(bytes data) public"]) + + expect(() => + toCallPolicy({ + policyVersion: CallPolicyVersion.V0_0_5, + permissions: [ + { + abi: testAbi, + target: zeroAddress, + functionName: "test", + args: [ + { + condition: ParamCondition.SLICE_EQUAL, + value: "0xff" + } + ] + } + ] + }) + ).toThrow("start and length are required for SLICE_EQUAL condition") + }, + TEST_TIMEOUT + ) + + test( + "should fail with SLICE_EQUAL condition, value is not equal to the given length", + async () => { + const testAbi = parseAbi(["function test(bytes data) public"]) + + expect(() => + toCallPolicy({ + policyVersion: CallPolicyVersion.V0_0_5, + permissions: [ + { + abi: testAbi, + target: zeroAddress, + functionName: "test", + args: [ + { + condition: ParamCondition.SLICE_EQUAL, + value: "0x00ff", + start: 1, + length: 1 + } + ] + } + ] + }) + ).toThrow("Value length is not equal to the given length") + }, + TEST_TIMEOUT + ) + + test( + "should fail with SLICE_EQUAL condition, value is too short", + async () => { + const testAbi = parseAbi(["function test(bytes data) public"]) + + expect(() => + toCallPolicy({ + policyVersion: CallPolicyVersion.V0_0_5, + permissions: [ + { + abi: testAbi, + target: zeroAddress, + functionName: "test", + args: [ + { + condition: ParamCondition.SLICE_EQUAL, + value: "0xffff", + start: 1, + length: 2 + } + ] + } + ] + }) + ).toThrow("Value is too short for the given start and length") + }, + TEST_TIMEOUT + ) + test( "Smart account client send transaction with serialization/deserialization", async () => { diff --git a/packages/test/v0.7/weightedValidatorWithPermission.test.ts b/packages/test/v0.7/weightedValidatorWithPermission.test.ts index 9153ffcd..8ab9b67c 100644 --- a/packages/test/v0.7/weightedValidatorWithPermission.test.ts +++ b/packages/test/v0.7/weightedValidatorWithPermission.test.ts @@ -73,7 +73,7 @@ describe("weightedValidator", () => { ) const callPolicy = toCallPolicy({ - policyVersion: CallPolicyVersion.V0_0_4, + policyVersion: CallPolicyVersion.V0_0_5, permissions: [ { target: zeroAddress, diff --git a/plugins/permission/constants.ts b/plugins/permission/constants.ts index 6eedc3a9..9fd619f3 100644 --- a/plugins/permission/constants.ts +++ b/plugins/permission/constants.ts @@ -48,6 +48,13 @@ export const CALL_POLICY_CONTRACT_V0_0_3 = export const CALL_POLICY_CONTRACT_V0_0_4 = "0x9a52283276A0ec8740DF50bF01B28A80D880eaf2" +/** + * @dev CALL_POLICY_CONTRACT_V0_0_5 updates + * - Added SLICE_EQUAL condition + */ +export const CALL_POLICY_CONTRACT_V0_0_5 = + "0x85770b902D1e503D5f5141d9eaC16d0d08eEaDd2" + export const GAS_POLICY_CONTRACT = "0xaeFC5AbC67FfD258abD0A3E54f65E70326F84b23" export const RATE_LIMIT_POLICY_CONTRACT = "0xf63d4139B25c836334edD76641356c6b74C86873" diff --git a/plugins/permission/policies/callPolicyUtils.ts b/plugins/permission/policies/callPolicyUtils.ts index bfc0e437..518eb9ae 100644 --- a/plugins/permission/policies/callPolicyUtils.ts +++ b/plugins/permission/policies/callPolicyUtils.ts @@ -5,7 +5,9 @@ import { type Hex, encodeAbiParameters, isHex, + keccak256, pad, + size, toFunctionSelector, toHex } from "viem" @@ -99,6 +101,7 @@ export function getPermissionFromABI< // Generate permission from the target function const functionSelector = toFunctionSelector(targetFunction) let paramRules: ParamRule[] = [] + if (args && Array.isArray(args)) { paramRules = (args as CombinedArgs) .map((arg, i) => { @@ -132,6 +135,41 @@ export function getPermissionFromABI< { size: 32 } ) ) + } else if (arg.condition === ParamCondition.SLICE_EQUAL) { + if (!("start" in arg) || !("length" in arg)) { + throw new Error( + "start and length are required for SLICE_EQUAL condition" + ) + } + const functionArgsType = targetFunction.inputs[i].type + const { start, length, value } = arg + + let hexValue: Hex + + // functionArgsType can be "string" or "bytes" + if (functionArgsType === "string") { + hexValue = toHex(value as Parameters[0]) + } else if (functionArgsType === "bytes") { + hexValue = isHex(value, { strict: true }) + ? value + : toHex(value as Parameters[0]) + } else { + throw new Error( + `Unsupported function argument type: ${functionArgsType} could be "string" or "bytes"` + ) + } + + if (size(hexValue) !== length) { + throw new Error( + "Value length is not equal to the given length" + ) + } + + params = [ + toHex(start, { size: 32 }), + toHex(length, { size: 32 }), + keccak256(hexValue) + ] } else { params = [ pad( @@ -144,6 +182,7 @@ export function getPermissionFromABI< ) ] } + return { params, offset: i * 32, diff --git a/plugins/permission/policies/toCallPolicy.ts b/plugins/permission/policies/toCallPolicy.ts index 613f811d..5d106dc4 100644 --- a/plugins/permission/policies/toCallPolicy.ts +++ b/plugins/permission/policies/toCallPolicy.ts @@ -5,6 +5,7 @@ import { CALL_POLICY_CONTRACT_V0_0_2, CALL_POLICY_CONTRACT_V0_0_3, CALL_POLICY_CONTRACT_V0_0_4, + CALL_POLICY_CONTRACT_V0_0_5, PolicyFlags } from "../constants.js" import type { Policy, PolicyParams } from "../types.js" @@ -18,7 +19,8 @@ export enum CallPolicyVersion { V0_0_1 = "0.0.1", V0_0_2 = "0.0.2", V0_0_3 = "0.0.3", - V0_0_4 = "0.0.4" + V0_0_4 = "0.0.4", + V0_0_5 = "0.0.5" } export const getCallPolicyAddress = ( @@ -35,6 +37,8 @@ export const getCallPolicyAddress = ( return CALL_POLICY_CONTRACT_V0_0_3 case CallPolicyVersion.V0_0_4: return CALL_POLICY_CONTRACT_V0_0_4 + case CallPolicyVersion.V0_0_5: + return CALL_POLICY_CONTRACT_V0_0_5 } } diff --git a/plugins/permission/policies/types.ts b/plugins/permission/policies/types.ts index 6bd95c87..587b9293 100644 --- a/plugins/permission/policies/types.ts +++ b/plugins/permission/policies/types.ts @@ -24,7 +24,8 @@ export enum ParamCondition { GREATER_THAN_OR_EQUAL = 3, LESS_THAN_OR_EQUAL = 4, NOT_EQUAL = 5, - ONE_OF = 6 + ONE_OF = 6, + SLICE_EQUAL = 7 } export interface ParamRule { @@ -113,6 +114,12 @@ type ConditionValue< condition: ParamCondition.ONE_OF value: AbiParameterToPrimitiveType[] } + | { + condition: ParamCondition.SLICE_EQUAL + value: AbiParameterToPrimitiveType + start: number + length: number + } | { condition: Exclude value: AbiParameterToPrimitiveType