diff --git a/.changeset/tiny-pugs-hunt.md b/.changeset/tiny-pugs-hunt.md new file mode 100644 index 00000000000..2aa5920a81a --- /dev/null +++ b/.changeset/tiny-pugs-hunt.md @@ -0,0 +1,20 @@ +--- +"thirdweb": minor +--- + +ERC20 Token Paymaster support + +You can now use ERC20 Token Paymasters with Smart Wallets. + +```typescript +import { base } from "thirdweb/chains"; +import { TokenPaymaster, smartWallet } from "thirdweb/wallets"; + +const wallet = smartWallet({ + chain: base, + sponsorGas: true, // only sponsor gas for the first ERC20 approval + overrides: { + tokenPaymaster: TokenPaymaster.BASE_USDC, + }, +}); +``` diff --git a/packages/thirdweb/src/exports/wallets/smart.ts b/packages/thirdweb/src/exports/wallets/smart.ts index f930bb45e50..b31c5430a14 100644 --- a/packages/thirdweb/src/exports/wallets/smart.ts +++ b/packages/thirdweb/src/exports/wallets/smart.ts @@ -35,4 +35,5 @@ export { ENTRYPOINT_ADDRESS_v0_7, DEFAULT_ACCOUNT_FACTORY_V0_6, DEFAULT_ACCOUNT_FACTORY_V0_7, + TokenPaymaster, } from "../../wallets/smart/lib/constants.js"; diff --git a/packages/thirdweb/src/wallets/smart/index.ts b/packages/thirdweb/src/wallets/smart/index.ts index c0ae9eaefcd..ca2e667e5a6 100644 --- a/packages/thirdweb/src/wallets/smart/index.ts +++ b/packages/thirdweb/src/wallets/smart/index.ts @@ -22,7 +22,6 @@ import type { PreparedTransaction } from "../../transaction/prepare-transaction. import { readContract } from "../../transaction/read-contract.js"; import { getAddress } from "../../utils/address.js"; import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js"; -import { concatHex } from "../../utils/encoding/helpers/concat-hex.js"; import type { Hex } from "../../utils/encoding/hex.js"; import { parseTypedData } from "../../utils/signatures/helpers/parseTypedData.js"; import type { @@ -45,7 +44,12 @@ import { prepareBatchExecute, prepareExecute, } from "./lib/calls.js"; -import { getDefaultAccountFactory } from "./lib/constants.js"; +import { + ENTRYPOINT_ADDRESS_v0_6, + ENTRYPOINT_ADDRESS_v0_7, + getDefaultAccountFactory, + getEntryPointVersion, +} from "./lib/constants.js"; import { clearAccountDeploying, createUnsignedUserOp, @@ -58,6 +62,7 @@ import type { SmartAccountOptions, SmartWalletConnectionOptions, SmartWalletOptions, + TokenPaymasterConfig, UserOperationV06, UserOperationV07, } from "./types.js"; @@ -116,6 +121,17 @@ export async function connectSmartWallet( } } + if ( + options.overrides?.tokenPaymaster && + !options.overrides?.entrypointAddress + ) { + // if token paymaster is set, but no entrypoint address, set the entrypoint address to v0.7 + options.overrides = { + ...options.overrides, + entrypointAddress: ENTRYPOINT_ADDRESS_v0_7, + }; + } + const factoryAddress = options.factoryAddress ?? getDefaultAccountFactory(options.overrides?.entrypointAddress); @@ -196,12 +212,24 @@ export async function disconnectSmartWallet( async function createSmartAccount( options: SmartAccountOptions, ): Promise { + const erc20Paymaster = options.overrides?.tokenPaymaster; + if (erc20Paymaster) { + if ( + getEntryPointVersion( + options.overrides?.entrypointAddress || ENTRYPOINT_ADDRESS_v0_6, + ) !== "v0.7" + ) { + throw new Error( + "Token paymaster is only supported for entrypoint version v0.7", + ); + } + } + const { accountContract } = options; const account: Account = { address: getAddress(accountContract.address), async sendTransaction(transaction: SendTransactionOption) { // if erc20 paymaster - check allowance and approve if needed - const erc20Paymaster = options.overrides?.erc20Paymaster; let paymasterOverride: | undefined | (( @@ -215,12 +243,7 @@ async function createSmartAccount( }); const paymasterCallback = async (): Promise => { return { - paymasterAndData: concatHex([ - erc20Paymaster.address as Hex, - erc20Paymaster?.token as Hex, - ]), - // for 0.7 compatibility - paymaster: erc20Paymaster.address as Hex, + paymaster: erc20Paymaster.paymasterAddress as Hex, paymasterData: "0x", }; }; @@ -436,13 +459,10 @@ async function createSmartAccount( async function approveERC20(args: { accountContract: ThirdwebContract; options: SmartAccountOptions; - erc20Paymaster: { - address: string; - token: string; - }; + erc20Paymaster: TokenPaymasterConfig; }) { const { accountContract, erc20Paymaster, options } = args; - const tokenAddress = erc20Paymaster.token; + const tokenAddress = erc20Paymaster.tokenAddress; const tokenContract = getContract({ address: tokenAddress, chain: accountContract.chain, @@ -451,7 +471,7 @@ async function approveERC20(args: { const accountAllowance = await allowance({ contract: tokenContract, owner: accountContract.address, - spender: erc20Paymaster.address, + spender: erc20Paymaster.paymasterAddress, }); if (accountAllowance > 0n) { @@ -460,7 +480,7 @@ async function approveERC20(args: { const approveTx = approve({ contract: tokenContract, - spender: erc20Paymaster.address, + spender: erc20Paymaster.paymasterAddress, amountWei: maxUint96 - 1n, }); const transaction = await toSerializableTransaction({ @@ -478,7 +498,7 @@ async function approveERC20(args: { ...options, overrides: { ...options.overrides, - erc20Paymaster: undefined, + tokenPaymaster: undefined, }, }, }); diff --git a/packages/thirdweb/src/wallets/smart/lib/bundler.ts b/packages/thirdweb/src/wallets/smart/lib/bundler.ts index a60f0bf895e..e4220b2b0e5 100644 --- a/packages/thirdweb/src/wallets/smart/lib/bundler.ts +++ b/packages/thirdweb/src/wallets/smart/lib/bundler.ts @@ -67,16 +67,26 @@ export async function bundleUserOp(args: { * ``` * @walletUtils */ -export async function estimateUserOpGas(args: { - userOp: UserOperationV06 | UserOperationV07; - options: BundlerOptions; -}): Promise { +export async function estimateUserOpGas( + args: { + userOp: UserOperationV06 | UserOperationV07; + options: BundlerOptions; + }, + stateOverrides?: { + [x: string]: { + stateDiff: { + [x: string]: `0x${string}`; + }; + }; + }, +): Promise { const res = await sendBundlerRequest({ ...args, operation: "eth_estimateUserOperationGas", params: [ hexlifyUserOp(args.userOp), args.options.entrypointAddress ?? ENTRYPOINT_ADDRESS_v0_6, + stateOverrides, ], }); diff --git a/packages/thirdweb/src/wallets/smart/lib/constants.ts b/packages/thirdweb/src/wallets/smart/lib/constants.ts index c0eaccefc15..cd61286b8c3 100644 --- a/packages/thirdweb/src/wallets/smart/lib/constants.ts +++ b/packages/thirdweb/src/wallets/smart/lib/constants.ts @@ -1,6 +1,7 @@ import type { Chain } from "../../../chains/types.js"; import { getAddress } from "../../../utils/address.js"; import { getThirdwebDomains } from "../../../utils/domains.js"; +import type { TokenPaymasterConfig } from "../types.js"; export const DUMMY_SIGNATURE = "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; @@ -17,6 +18,28 @@ export const ENTRYPOINT_ADDRESS_v0_7 = export const MANAGED_ACCOUNT_GAS_BUFFER = 50000n; +type PAYMASTERS = "BASE_USDC" | "CELO_CUSD" | "LISK_LSK"; +export const TokenPaymaster: Record = { + BASE_USDC: { + chainId: 8453, + paymasterAddress: "0x2222f2738BE6bB7aA0Bfe4AEeAf2908172CF5539", + tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + balanceStorageSlot: 9n, + }, + CELO_CUSD: { + chainId: 42220, + paymasterAddress: "0x3feA3c5744D715ff46e91C4e5C9a94426DfF2aF9", + tokenAddress: "0x765DE816845861e75A25fCA122bb6898B8B1282a", + balanceStorageSlot: 9n, + }, + LISK_LSK: { + chainId: 1135, + paymasterAddress: "0x9eb8cf7fBa5ed9EeDCC97a0d52254cc0e9B1AC25", + tokenAddress: "0xac485391EB2d7D88253a7F1eF18C37f4242D1A24", + balanceStorageSlot: 9n, + }, +}; + /* * @internal */ diff --git a/packages/thirdweb/src/wallets/smart/lib/userop.ts b/packages/thirdweb/src/wallets/smart/lib/userop.ts index 755cc0fbde9..8e0a9521cd1 100644 --- a/packages/thirdweb/src/wallets/smart/lib/userop.ts +++ b/packages/thirdweb/src/wallets/smart/lib/userop.ts @@ -1,4 +1,5 @@ -import { concat } from "viem"; +import { maxUint96 } from "ox/Solidity"; +import { concat, keccak256, toHex } from "viem"; import type { Chain } from "../../../chains/types.js"; import type { ThirdwebClient } from "../../../client/client.js"; import { @@ -13,6 +14,7 @@ import { encode } from "../../../transaction/actions/encode.js"; import { toSerializableTransaction } from "../../../transaction/actions/to-serializable-transaction.js"; import type { PreparedTransaction } from "../../../transaction/prepare-transaction.js"; import type { TransactionReceipt } from "../../../transaction/types.js"; +import { encodeAbiParameters } from "../../../utils/abi/encodeAbiParameters.js"; import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js"; import type { Hex } from "../../../utils/encoding/hex.js"; import { hexToBytes } from "../../../utils/encoding/to-bytes.js"; @@ -361,17 +363,38 @@ async function populateUserOp_v0_7(args: { paymasterResult.paymasterVerificationGasLimit; } else { // otherwise fallback to bundler for gas limits - const estimates = await estimateUserOpGas({ - userOp: partialOp, - options: bundlerOptions, - }); + const stateOverrides = overrides?.tokenPaymaster + ? { + [overrides.tokenPaymaster.tokenAddress]: { + stateDiff: { + [keccak256( + encodeAbiParameters( + [{ type: "address" }, { type: "uint256" }], + [ + accountContract.address, + overrides.tokenPaymaster.balanceStorageSlot, + ], + ), + )]: toHex(maxUint96, { size: 32 }), + }, + }, + } + : undefined; + const estimates = await estimateUserOpGas( + { + userOp: partialOp, + options: bundlerOptions, + }, + stateOverrides, + ); partialOp.callGasLimit = estimates.callGasLimit; partialOp.verificationGasLimit = estimates.verificationGasLimit; partialOp.preVerificationGas = estimates.preVerificationGas; - partialOp.paymasterPostOpGasLimit = - paymasterResult.paymasterPostOpGasLimit || 0n; + partialOp.paymasterPostOpGasLimit = overrides?.tokenPaymaster + ? 500000n // TODO: estimate this better, needed if there's an extra swap needed in the paymaster + : estimates.paymasterPostOpGasLimit || 0n; partialOp.paymasterVerificationGasLimit = - paymasterResult.paymasterVerificationGasLimit || 0n; + estimates.paymasterVerificationGasLimit || 0n; // need paymaster to re-sign after estimates const paymasterResult2 = (await getPaymasterAndData({ userOp: partialOp, diff --git a/packages/thirdweb/src/wallets/smart/smart-wallet-tokenpaymaster.test.ts b/packages/thirdweb/src/wallets/smart/smart-wallet-tokenpaymaster.test.ts new file mode 100644 index 00000000000..1ae1c23f126 --- /dev/null +++ b/packages/thirdweb/src/wallets/smart/smart-wallet-tokenpaymaster.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, it } from "vitest"; +import { TEST_CLIENT } from "../../../test/src/test-clients.js"; +import { base } from "../../chains/chain-definitions/base.js"; +import { celo } from "../../chains/chain-definitions/celo.js"; +import { defineChain } from "../../chains/utils.js"; +import { sendTransaction } from "../../transaction/actions/send-transaction.js"; +import { prepareTransaction } from "../../transaction/prepare-transaction.js"; +import { privateKeyToAccount } from "../private-key.js"; +import { TokenPaymaster } from "./lib/constants.js"; +import { smartWallet } from "./smart-wallet.js"; + +const client = TEST_CLIENT; + +describe.runIf(process.env.TW_SECRET_KEY).skip.sequential( + "SmartWallet token paymaster tests", + { + retry: 0, + timeout: 240_000, + }, + () => { + it.skip("should send a transaction with base usdc", async () => { + const chain = base; + const tokenPaymaster = TokenPaymaster.BASE_USDC; + const personalAccount = privateKeyToAccount({ + client, + privateKey: + "edf401e8ddbb743f3353b055081cb220ce4c5c04e08da162d86e0dba7c6f0f01", // 0xa470E7c88611364f55B2d7912613e10AF2eA918D + }); + const wallet = smartWallet({ + chain, + gasless: true, + overrides: { + tokenPaymaster, + }, + }); + const smartAccount = await wallet.connect({ + client: TEST_CLIENT, + personalAccount, + }); + const tx = prepareTransaction({ + client, + chain, + to: smartAccount.address, + value: 0n, + }); + const receipt = await sendTransaction({ + transaction: tx, + account: smartAccount, + }); + expect(receipt.transactionHash).toBeDefined(); + }); + + it.skip("should send a transaction with base celo", async () => { + const chain = celo; + const tokenPaymaster = TokenPaymaster.CELO_CUSD; + const personalAccount = privateKeyToAccount({ + client, + privateKey: + "edf401e8ddbb743f3353b055081cb220ce4c5c04e08da162d86e0dba7c6f0f01", // 0xa470E7c88611364f55B2d7912613e10AF2eA918D + }); + const wallet = smartWallet({ + chain, + gasless: true, + overrides: { + tokenPaymaster, + }, + }); + const smartAccount = await wallet.connect({ + client: TEST_CLIENT, + personalAccount, + }); + const tx = prepareTransaction({ + client, + chain, + to: smartAccount.address, + value: 0n, + }); + const receipt = await sendTransaction({ + transaction: tx, + account: smartAccount, + }); + expect(receipt.transactionHash).toBeDefined(); + }); + + it("should send a transaction with base lisk", async () => { + const chain = defineChain(1135); + const tokenPaymaster = TokenPaymaster.LISK_LSK; + const personalAccount = privateKeyToAccount({ + client, + privateKey: + "edf401e8ddbb743f3353b055081cb220ce4c5c04e08da162d86e0dba7c6f0f01", // 0xa470E7c88611364f55B2d7912613e10AF2eA918D + }); + const wallet = smartWallet({ + chain, + gasless: true, + overrides: { + tokenPaymaster, + }, + }); + const smartAccount = await wallet.connect({ + client: TEST_CLIENT, + personalAccount, + }); + const tx = prepareTransaction({ + client, + chain, + to: smartAccount.address, + value: 0n, + }); + const receipt = await sendTransaction({ + transaction: tx, + account: smartAccount, + }); + expect(receipt.transactionHash).toBeDefined(); + }); + }, +); diff --git a/packages/thirdweb/src/wallets/smart/smart-wallet.ts b/packages/thirdweb/src/wallets/smart/smart-wallet.ts index a63b233f939..356285ee299 100644 --- a/packages/thirdweb/src/wallets/smart/smart-wallet.ts +++ b/packages/thirdweb/src/wallets/smart/smart-wallet.ts @@ -118,7 +118,7 @@ import { getDefaultAccountFactory } from "./lib/constants.js"; * accountAddress: "0x...", // override account address * accountSalt: "0x...", // override account salt * entrypointAddress: "0x...", // override entrypoint address - * erc20Paymaster: { ... }, // enable erc20 paymaster + * tokenPaymaster: TokenPaymaster.BASE_USDC, // enable erc20 paymaster * bundlerUrl: "https://...", // override bundler url * paymaster: (userOp) => { ... }, // override paymaster * ... diff --git a/packages/thirdweb/src/wallets/smart/types.ts b/packages/thirdweb/src/wallets/smart/types.ts index b29877b7651..9dfe7a172c0 100644 --- a/packages/thirdweb/src/wallets/smart/types.ts +++ b/packages/thirdweb/src/wallets/smart/types.ts @@ -8,6 +8,13 @@ import type { Hex } from "../../utils/encoding/hex.js"; import type { Prettify } from "../../utils/type-utils.js"; import type { Account, SendTransactionOption } from "../interfaces/wallet.js"; +export type TokenPaymasterConfig = { + chainId: number; + paymasterAddress: string; + tokenAddress: string; + balanceStorageSlot: bigint; +}; + export type SmartWalletOptions = Prettify< { chain: Chain; // TODO consider making default chain optional @@ -17,10 +24,7 @@ export type SmartWalletOptions = Prettify< accountAddress?: string; accountSalt?: string; entrypointAddress?: string; - erc20Paymaster?: { - address: string; - token: string; - }; + tokenPaymaster?: TokenPaymasterConfig; paymaster?: ( userOp: UserOperationV06 | UserOperationV07, ) => Promise;