diff --git a/.changeset/long-walls-hang.md b/.changeset/long-walls-hang.md new file mode 100644 index 00000000..ae2b5960 --- /dev/null +++ b/.changeset/long-walls-hang.md @@ -0,0 +1,5 @@ +--- +"permissionless": patch +--- + +Added utils to create erc20 state overrides diff --git a/.changeset/modern-lies-count.md b/.changeset/modern-lies-count.md new file mode 100644 index 00000000..c0f085bd --- /dev/null +++ b/.changeset/modern-lies-count.md @@ -0,0 +1,5 @@ +--- +"permissionless": patch +--- + +Added balanceOverride to prepareUserOperationForErc20Paymaster diff --git a/bun.lockb b/bun.lockb index 898946e9..5b9ea537 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 102ca2d6..53dde4cd 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "get-port": "^7.0.0", "tsc-alias": "^1.8.8", "vitest": "^1.2.0", - "viem": "^2.21.2", + "viem": "2.21.22", "wagmi": "^2.12.8", "@permissionless/wagmi": "workspace:packages/wagmi", "@types/react": "^18.3.1", diff --git a/packages/permissionless/actions/pimlico/getTokenQuotes.ts b/packages/permissionless/actions/pimlico/getTokenQuotes.ts index daa6dd18..7c054621 100644 --- a/packages/permissionless/actions/pimlico/getTokenQuotes.ts +++ b/packages/permissionless/actions/pimlico/getTokenQuotes.ts @@ -25,6 +25,8 @@ export type GetTokenQuotesReturnType = { postOpGas: bigint exchangeRate: bigint exchangeRateNativeToUsd: bigint + balanceSlot?: bigint + allowanceSlot?: bigint }[] /** @@ -62,6 +64,12 @@ export const getTokenQuotes = async < return res.quotes.map((quote) => ({ ...quote, + balanceSlot: quote.balanceSlot + ? hexToBigInt(quote.balanceSlot) + : undefined, + allowanceSlot: quote.allowanceSlot + ? hexToBigInt(quote.allowanceSlot) + : undefined, postOpGas: hexToBigInt(quote.postOpGas), exchangeRate: hexToBigInt(quote.exchangeRate), exchangeRateNativeToUsd: hexToBigInt(quote.exchangeRateNativeToUsd) diff --git a/packages/permissionless/experimental/pimlico/utils/prepareUserOperationForErc20Paymaster.test.ts b/packages/permissionless/experimental/pimlico/utils/prepareUserOperationForErc20Paymaster.test.ts index add364b1..54bfc051 100644 --- a/packages/permissionless/experimental/pimlico/utils/prepareUserOperationForErc20Paymaster.test.ts +++ b/packages/permissionless/experimental/pimlico/utils/prepareUserOperationForErc20Paymaster.test.ts @@ -191,5 +191,93 @@ describe.each(getCoreSmartAccounts())( expect(FINAL_ETH_BALANCE).toEqual(INTIAL_ETH_BALANCE) // There should be no ETH balance change } ) + + testWithRpc.skipIf(!supportsEntryPointV07)( + "prepareUserOperationForErc20Paymaster_v07", + async ({ rpc }) => { + const { anvilRpc } = rpc + + const account = ( + await getSmartAccountClient({ + entryPoint: { + version: "0.7" + }, + ...rpc + }) + ).account + + const publicClient = getPublicClient(anvilRpc) + + const pimlicoClient = createPimlicoClient({ + transport: http(rpc.paymasterRpc), + entryPoint: { + address: entryPoint07Address, + version: "0.7" + } + }) + + const smartAccountClient = createSmartAccountClient({ + // @ts-ignore + client: getPublicClient(anvilRpc), + account, + paymaster: pimlicoClient, + chain: foundry, + userOperation: { + prepareUserOperation: + prepareUserOperationForErc20Paymaster( + pimlicoClient, + { + balanceOverride: true + } + ) + }, + bundlerTransport: http(rpc.altoRpc) + }) + + const INITIAL_TOKEN_BALANCE = parseEther("100") + const INTIAL_ETH_BALANCE = await publicClient.getBalance({ + address: smartAccountClient.account.address + }) + + sudoMintTokens({ + amount: INITIAL_TOKEN_BALANCE, + to: smartAccountClient.account.address, + anvilRpc + }) + + const opHash = await smartAccountClient.sendUserOperation({ + calls: [ + { + to: zeroAddress, + data: "0x", + value: 0n + } + ], + paymasterContext: { + token: ERC20_ADDRESS + } + }) + + const receipt = + await smartAccountClient.waitForUserOperationReceipt({ + hash: opHash + }) + + expect(receipt).toBeTruthy() + expect(receipt).toBeTruthy() + expect(receipt.success).toBeTruthy() + + const FINAL_TOKEN_BALANCE = await tokenBalanceOf( + smartAccountClient.account.address, + rpc.anvilRpc + ) + const FINAL_ETH_BALANCE = await publicClient.getBalance({ + address: smartAccountClient.account.address + }) + + expect(FINAL_TOKEN_BALANCE).toBeLessThan(INITIAL_TOKEN_BALANCE) // Token balance should be deducted + expect(FINAL_ETH_BALANCE).toEqual(INTIAL_ETH_BALANCE) // There should be no ETH balance change + } + ) } ) diff --git a/packages/permissionless/experimental/pimlico/utils/prepareUserOperationForErc20Paymaster.ts b/packages/permissionless/experimental/pimlico/utils/prepareUserOperationForErc20Paymaster.ts index df3c4993..60c304c0 100644 --- a/packages/permissionless/experimental/pimlico/utils/prepareUserOperationForErc20Paymaster.ts +++ b/packages/permissionless/experimental/pimlico/utils/prepareUserOperationForErc20Paymaster.ts @@ -3,6 +3,7 @@ import { type Chain, type Client, type ContractFunctionParameters, + RpcError, type Transport, encodeFunctionData, erc20Abi, @@ -24,9 +25,21 @@ import { getChainId as getChainId_ } from "viem/actions" import { readContract } from "viem/actions" import { getAction, parseAccount } from "viem/utils" import { getTokenQuotes } from "../../../actions/pimlico" +import { erc20AllowanceOverride, erc20BalanceOverride } from "../../../utils" export const prepareUserOperationForErc20Paymaster = - (pimlicoClient: Client) => + ( + pimlicoClient: Client, + { + balanceOverride = false, + balanceSlot: _balanceSlot, + allowanceSlot: _allowanceSlot + }: { + balanceOverride?: boolean + balanceSlot?: bigint + allowanceSlot?: bigint + } = {} + ) => async < account extends SmartAccount | undefined, const calls extends readonly unknown[], @@ -95,6 +108,13 @@ export const prepareUserOperationForErc20Paymaster = entryPointAddress: account.entryPoint.address }) + if (quotes.length === 0) { + throw new RpcError(new Error("Quotes not found"), { + shortMessage: + "client didn't return token quotes, check if the token is supported" + }) + } + const { postOpGas, exchangeRate, @@ -118,9 +138,55 @@ export const prepareUserOperationForErc20Paymaster = } //////////////////////////////////////////////////////////////////////////////// + // Call prepareUserOperation //////////////////////////////////////////////////////////////////////////////// + const allowanceSlot = _allowanceSlot ?? quotes[0].allowanceSlot + const balanceSlot = _balanceSlot ?? quotes[0].balanceSlot + + const hasSlot = allowanceSlot && balanceSlot + + if (!hasSlot && balanceOverride) { + throw new Error( + `balanceOverride is not supported for token ${token}, provide custom slot for balance & allowance overrides` + ) + } + + const balanceStateOverride = + balanceOverride && balanceSlot + ? erc20BalanceOverride({ + token, + owner: account.address, + slot: balanceSlot + })[0] + : undefined + + const allowanceStateOverride = + balanceOverride && allowanceSlot + ? erc20AllowanceOverride({ + token, + owner: account.address, + spender: paymasterERC20Address, + slot: allowanceSlot + })[0] + : undefined + + parameters.stateOverride = + balanceOverride && + balanceStateOverride && + allowanceStateOverride // allowanceSlot && balanceSlot is cuz of TypeScript :/ + ? (parameters.stateOverride ?? []).concat([ + { + address: token, + stateDiff: [ + ...(allowanceStateOverride.stateDiff ?? []), + ...(balanceStateOverride.stateDiff ?? []) + ] + } + ]) + : parameters.stateOverride + const userOperation = await getAction( client, prepareUserOperation, diff --git a/packages/permissionless/package.json b/packages/permissionless/package.json index 2453453b..76f3f6ba 100644 --- a/packages/permissionless/package.json +++ b/packages/permissionless/package.json @@ -71,6 +71,6 @@ } }, "peerDependencies": { - "viem": "^2.21.2" + "viem": "^2.21.22" } } diff --git a/packages/permissionless/types/pimlico.ts b/packages/permissionless/types/pimlico.ts index 8af12de0..ac7c70d0 100644 --- a/packages/permissionless/types/pimlico.ts +++ b/packages/permissionless/types/pimlico.ts @@ -35,6 +35,8 @@ type GetTokenQuotesWithBigIntAsHex = { postOpGas: Hex exchangeRate: Hex exchangeRateNativeToUsd: Hex + balanceSlot?: Hex + allowanceSlot?: Hex }[] } diff --git a/packages/permissionless/utils/erc20AllowanceOverride.test.ts b/packages/permissionless/utils/erc20AllowanceOverride.test.ts new file mode 100644 index 00000000..a1bdc7eb --- /dev/null +++ b/packages/permissionless/utils/erc20AllowanceOverride.test.ts @@ -0,0 +1,59 @@ +import { toHex } from "viem" +import { describe, expect, test } from "vitest" +import { + type Erc20AllowanceOverrideParameters, + erc20AllowanceOverride +} from "./erc20AllowanceOverride" + +describe("erc20AllowanceOverride", () => { + test("should return the correct structure for valid inputs", () => { + const params = { + token: "0xTokenAddress", + owner: "0xOwnerAddress", + spender: "0xSpenderAddress", + slot: BigInt(1), + amount: BigInt(100) + } as const + + const result = erc20AllowanceOverride(params) + + expect(result).toEqual([ + { + address: params.token, + stateDiff: [ + { + slot: expect.any(String), // Slot will be a keccak256 hash + value: toHex(params.amount) + } + ] + } + ]) + }) + + test("should use the default amount when none is provided", () => { + const params: Erc20AllowanceOverrideParameters = { + token: "0xTokenAddress", + owner: "0xOwnerAddress", + spender: "0xSpenderAddress", + slot: BigInt(1) + } + + const result = erc20AllowanceOverride(params) + + const expectedDefaultAmount = BigInt( + "0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + ) + + expect(result).toEqual([ + { + address: params.token, + stateDiff: [ + { + slot: expect.any(String), // Slot will be a keccak256 hash + value: toHex(expectedDefaultAmount) + } + ] + } + ]) + }) +}) diff --git a/packages/permissionless/utils/erc20AllowanceOverride.ts b/packages/permissionless/utils/erc20AllowanceOverride.ts new file mode 100644 index 00000000..6adf5f58 --- /dev/null +++ b/packages/permissionless/utils/erc20AllowanceOverride.ts @@ -0,0 +1,66 @@ +import { + type Address, + type StateOverride, + encodeAbiParameters, + keccak256, + toHex +} from "viem" + +export type Erc20AllowanceOverrideParameters = { + token: Address + owner: Address + spender: Address + slot: bigint + amount?: bigint +} + +export function erc20AllowanceOverride({ + token, + owner, + spender, + slot, + amount = BigInt( + "0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + ) +}: Erc20AllowanceOverrideParameters): StateOverride { + const smartAccountErc20AllowanceSlot = keccak256( + encodeAbiParameters( + [ + { + type: "address" + }, + { + type: "bytes32" + } + ], + [ + spender, + keccak256( + encodeAbiParameters( + [ + { + type: "address" + }, + { + type: "uint256" + } + ], + [owner, BigInt(slot)] + ) + ) + ] + ) + ) + + return [ + { + address: token, + stateDiff: [ + { + slot: smartAccountErc20AllowanceSlot, + value: toHex(amount) + } + ] + } + ] +} diff --git a/packages/permissionless/utils/erc20BalanceOverride.test.ts b/packages/permissionless/utils/erc20BalanceOverride.test.ts new file mode 100644 index 00000000..2262b5c4 --- /dev/null +++ b/packages/permissionless/utils/erc20BalanceOverride.test.ts @@ -0,0 +1,57 @@ +import { toHex } from "viem" +import { describe, expect, test } from "vitest" +import { + type Erc20BalanceOverrideParameters, + erc20BalanceOverride +} from "./erc20BalanceOverride" + +describe("erc20BalanceOverride", () => { + test("should return the correct structure for valid inputs", () => { + const params = { + token: "0xTokenAddress", + owner: "0xOwnerAddress", + slot: BigInt(1), + balance: BigInt(1000) + } as const + + const result = erc20BalanceOverride(params) + + expect(result).toEqual([ + { + address: params.token, + stateDiff: [ + { + slot: expect.any(String), // Slot will be a keccak256 hash + value: toHex(params.balance) + } + ] + } + ]) + }) + + test("should use the default balance when none is provided", () => { + const params: Erc20BalanceOverrideParameters = { + token: "0xTokenAddress", + owner: "0xOwnerAddress", + slot: BigInt(1) + } + + const result = erc20BalanceOverride(params) + + const expectedDefaultBalance = BigInt( + "0x100000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + ) + + expect(result).toEqual([ + { + address: params.token, + stateDiff: [ + { + slot: expect.any(String), // Slot will be a keccak256 hash + value: toHex(expectedDefaultBalance) + } + ] + } + ]) + }) +}) diff --git a/packages/permissionless/utils/erc20BalanceOverride.ts b/packages/permissionless/utils/erc20BalanceOverride.ts new file mode 100644 index 00000000..10be16e2 --- /dev/null +++ b/packages/permissionless/utils/erc20BalanceOverride.ts @@ -0,0 +1,49 @@ +import { + type Address, + type StateOverride, + encodeAbiParameters, + keccak256, + toHex +} from "viem" + +export type Erc20BalanceOverrideParameters = { + token: Address + owner: Address + slot: bigint + balance?: bigint +} + +export function erc20BalanceOverride({ + token, + owner, + slot, + balance = BigInt( + "0x100000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + ) +}: Erc20BalanceOverrideParameters): StateOverride { + const smartAccountErc20BalanceSlot = keccak256( + encodeAbiParameters( + [ + { + type: "address" + }, + { + type: "uint256" + } + ], + [owner, slot] + ) + ) + + return [ + { + address: token, + stateDiff: [ + { + slot: smartAccountErc20BalanceSlot, + value: toHex(balance) + } + ] + } + ] +} diff --git a/packages/permissionless/utils/index.ts b/packages/permissionless/utils/index.ts index 8469d094..c388de49 100644 --- a/packages/permissionless/utils/index.ts +++ b/packages/permissionless/utils/index.ts @@ -17,6 +17,14 @@ import { import { getPackedUserOperation } from "./getPackedUserOperation" import { type EncodeCallDataParams, encode7579Calls } from "./encode7579Calls" +import { + type Erc20AllowanceOverrideParameters, + erc20AllowanceOverride +} from "./erc20AllowanceOverride" +import { + type Erc20BalanceOverrideParameters, + erc20BalanceOverride +} from "./erc20BalanceOverride" export { transactionReceiptStatus, @@ -32,5 +40,9 @@ export { type EncodeInstallModuleParameters, encodeInstallModule, type EncodeCallDataParams, - encode7579Calls + encode7579Calls, + erc20AllowanceOverride, + erc20BalanceOverride, + type Erc20AllowanceOverrideParameters, + type Erc20BalanceOverrideParameters }