From 33d6516d06aa0a87f3dd2408981380e2c8dcda86 Mon Sep 17 00:00:00 2001 From: Justin Zen Date: Fri, 5 Sep 2025 18:40:42 +0800 Subject: [PATCH 1/4] feat: add substr call policy --- plugins/permission/constants.ts | 7 +++++++ plugins/permission/policies/callPolicyUtils.ts | 17 +++++++++++++++++ plugins/permission/policies/toCallPolicy.ts | 6 +++++- plugins/permission/policies/types.ts | 9 ++++++++- 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/plugins/permission/constants.ts b/plugins/permission/constants.ts index 6eedc3a9..263ac8de 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 = + "0xe83c72b75d73c6505b2ddb17032df978aefe4e4b" + 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..f07b0e31 100644 --- a/plugins/permission/policies/callPolicyUtils.ts +++ b/plugins/permission/policies/callPolicyUtils.ts @@ -5,6 +5,7 @@ import { type Hex, encodeAbiParameters, isHex, + keccak256, pad, toFunctionSelector, toHex @@ -132,6 +133,22 @@ 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 { start, length, value } = arg + params = [ + toHex(start, { size: 32 }), + toHex(length, { size: 32 }), + keccak256( + isHex(value) + ? value + : toHex(value as Parameters[0]) + ) + ] } else { params = [ pad( 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 From 592842f289b53742b9e23942672d095ed05e4233 Mon Sep 17 00:00:00 2001 From: Justin Zen Date: Mon, 8 Sep 2025 14:40:10 +0800 Subject: [PATCH 2/4] fix: slice equal hash --- .../test/v0.7/permissionValidator.test.ts | 222 ++++++++++++++++++ .../weightedValidatorWithPermission.test.ts | 2 +- .../permission/policies/callPolicyUtils.ts | 20 +- 3 files changed, 238 insertions(+), 6 deletions(-) diff --git a/packages/test/v0.7/permissionValidator.test.ts b/packages/test/v0.7/permissionValidator.test.ts index 335b02e4..99df3feb 100644 --- a/packages/test/v0.7/permissionValidator.test.ts +++ b/packages/test/v0.7/permissionValidator.test.ts @@ -25,6 +25,8 @@ import { getAbiItem, hashMessage, hashTypedData, + pad, + parseAbi, parseEther, toFunctionSelector, zeroAddress @@ -1229,6 +1231,226 @@ 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", + 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: "0xffff", + 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( + "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: "0xffff", + 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: "0xffff" + } + ] + } + ] + }) + ).toThrow("start and length are required for SLICE_EQUAL condition") + }, + 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/policies/callPolicyUtils.ts b/plugins/permission/policies/callPolicyUtils.ts index f07b0e31..39f8f831 100644 --- a/plugins/permission/policies/callPolicyUtils.ts +++ b/plugins/permission/policies/callPolicyUtils.ts @@ -7,6 +7,8 @@ import { isHex, keccak256, pad, + size, + slice, toFunctionSelector, toHex } from "viem" @@ -100,6 +102,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) => { @@ -140,14 +143,20 @@ export function getPermissionFromABI< ) } const { start, length, value } = arg + const hexValue = isHex(value) + ? value + : toHex(value as Parameters[0]) + + if (size(hexValue) < start + length) { + throw new Error( + "Value is too short for the given start and length" + ) + } + params = [ toHex(start, { size: 32 }), toHex(length, { size: 32 }), - keccak256( - isHex(value) - ? value - : toHex(value as Parameters[0]) - ) + keccak256(slice(hexValue, start, start + length)) ] } else { params = [ @@ -161,6 +170,7 @@ export function getPermissionFromABI< ) ] } + return { params, offset: i * 32, From 715fddf6a166b1d2cfcbeb76cd81e304e2c37fa6 Mon Sep 17 00:00:00 2001 From: Justin Zen Date: Tue, 9 Sep 2025 21:37:34 +0800 Subject: [PATCH 3/4] chore: update policy address --- plugins/permission/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/permission/constants.ts b/plugins/permission/constants.ts index 263ac8de..9fd619f3 100644 --- a/plugins/permission/constants.ts +++ b/plugins/permission/constants.ts @@ -53,7 +53,7 @@ export const CALL_POLICY_CONTRACT_V0_0_4 = * - Added SLICE_EQUAL condition */ export const CALL_POLICY_CONTRACT_V0_0_5 = - "0xe83c72b75d73c6505b2ddb17032df978aefe4e4b" + "0x85770b902D1e503D5f5141d9eaC16d0d08eEaDd2" export const GAS_POLICY_CONTRACT = "0xaeFC5AbC67FfD258abD0A3E54f65E70326F84b23" export const RATE_LIMIT_POLICY_CONTRACT = From 9520b3ffa0b6d9ccc1912465516a50cec3bf12e4 Mon Sep 17 00:00:00 2001 From: Justin Zen Date: Wed, 10 Sep 2025 11:13:52 +0800 Subject: [PATCH 4/4] fix: quoted value of slice policy --- .../test/v0.7/permissionValidator.test.ts | 136 +++++++++++++++++- .../permission/policies/callPolicyUtils.ts | 26 +++- 2 files changed, 151 insertions(+), 11 deletions(-) diff --git a/packages/test/v0.7/permissionValidator.test.ts b/packages/test/v0.7/permissionValidator.test.ts index 99df3feb..fe4679ba 100644 --- a/packages/test/v0.7/permissionValidator.test.ts +++ b/packages/test/v0.7/permissionValidator.test.ts @@ -25,6 +25,7 @@ import { getAbiItem, hashMessage, hashTypedData, + hexToBytes, pad, parseAbi, parseEther, @@ -1304,7 +1305,7 @@ describe("Permission kernel Account", () => { ) test( - "Smart account client send transaction with SLICE_EQUAL condition", + "Smart account client send transaction with SLICE_EQUAL condition with bytes", async () => { const testAbi = parseAbi(["function test(bytes data) public"]) @@ -1318,7 +1319,7 @@ describe("Permission kernel Account", () => { args: [ { condition: ParamCondition.SLICE_EQUAL, - value: "0xffff", + value: "0xff", start: 1, length: 1 } @@ -1350,6 +1351,104 @@ describe("Permission kernel Account", () => { 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 () => { @@ -1365,7 +1464,7 @@ describe("Permission kernel Account", () => { args: [ { condition: ParamCondition.SLICE_EQUAL, - value: "0xffff", + value: "0xff", start: 1, length: 1 } @@ -1411,7 +1510,7 @@ describe("Permission kernel Account", () => { args: [ { condition: ParamCondition.SLICE_EQUAL, - value: "0xffff" + value: "0xff" } ] } @@ -1422,6 +1521,35 @@ describe("Permission kernel Account", () => { 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 () => { diff --git a/plugins/permission/policies/callPolicyUtils.ts b/plugins/permission/policies/callPolicyUtils.ts index 39f8f831..518eb9ae 100644 --- a/plugins/permission/policies/callPolicyUtils.ts +++ b/plugins/permission/policies/callPolicyUtils.ts @@ -8,7 +8,6 @@ import { keccak256, pad, size, - slice, toFunctionSelector, toHex } from "viem" @@ -142,21 +141,34 @@ export function getPermissionFromABI< "start and length are required for SLICE_EQUAL condition" ) } + const functionArgsType = targetFunction.inputs[i].type const { start, length, value } = arg - const hexValue = isHex(value) - ? value - : toHex(value as Parameters[0]) - if (size(hexValue) < start + length) { + 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 is too short for the given start and length" + "Value length is not equal to the given length" ) } params = [ toHex(start, { size: 32 }), toHex(length, { size: 32 }), - keccak256(slice(hexValue, start, start + length)) + keccak256(hexValue) ] } else { params = [