Skip to content

Commit d1aa380

Browse files
TokenPaymaster (BASE_USDC, CELO_USD, LISK_LSK) (#5634)
Co-authored-by: 0xFirekeeper <0xFirekeeper@gmail.com>
1 parent c186f63 commit d1aa380

File tree

9 files changed

+252
-34
lines changed

9 files changed

+252
-34
lines changed

.changeset/tiny-pugs-hunt.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
ERC20 Token Paymaster support
6+
7+
You can now use ERC20 Token Paymasters with Smart Wallets.
8+
9+
```typescript
10+
import { base } from "thirdweb/chains";
11+
import { TokenPaymaster, smartWallet } from "thirdweb/wallets";
12+
13+
const wallet = smartWallet({
14+
chain: base,
15+
sponsorGas: true, // only sponsor gas for the first ERC20 approval
16+
overrides: {
17+
tokenPaymaster: TokenPaymaster.BASE_USDC,
18+
},
19+
});
20+
```

packages/thirdweb/src/exports/wallets/smart.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@ export {
3535
ENTRYPOINT_ADDRESS_v0_7,
3636
DEFAULT_ACCOUNT_FACTORY_V0_6,
3737
DEFAULT_ACCOUNT_FACTORY_V0_7,
38+
TokenPaymaster,
3839
} from "../../wallets/smart/lib/constants.js";

packages/thirdweb/src/wallets/smart/index.ts

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import type { PreparedTransaction } from "../../transaction/prepare-transaction.
2222
import { readContract } from "../../transaction/read-contract.js";
2323
import { getAddress } from "../../utils/address.js";
2424
import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js";
25-
import { concatHex } from "../../utils/encoding/helpers/concat-hex.js";
2625
import type { Hex } from "../../utils/encoding/hex.js";
2726
import { parseTypedData } from "../../utils/signatures/helpers/parseTypedData.js";
2827
import type {
@@ -45,7 +44,12 @@ import {
4544
prepareBatchExecute,
4645
prepareExecute,
4746
} from "./lib/calls.js";
48-
import { getDefaultAccountFactory } from "./lib/constants.js";
47+
import {
48+
ENTRYPOINT_ADDRESS_v0_6,
49+
ENTRYPOINT_ADDRESS_v0_7,
50+
getDefaultAccountFactory,
51+
getEntryPointVersion,
52+
} from "./lib/constants.js";
4953
import {
5054
clearAccountDeploying,
5155
createUnsignedUserOp,
@@ -58,6 +62,7 @@ import type {
5862
SmartAccountOptions,
5963
SmartWalletConnectionOptions,
6064
SmartWalletOptions,
65+
TokenPaymasterConfig,
6166
UserOperationV06,
6267
UserOperationV07,
6368
} from "./types.js";
@@ -116,6 +121,17 @@ export async function connectSmartWallet(
116121
}
117122
}
118123

124+
if (
125+
options.overrides?.tokenPaymaster &&
126+
!options.overrides?.entrypointAddress
127+
) {
128+
// if token paymaster is set, but no entrypoint address, set the entrypoint address to v0.7
129+
options.overrides = {
130+
...options.overrides,
131+
entrypointAddress: ENTRYPOINT_ADDRESS_v0_7,
132+
};
133+
}
134+
119135
const factoryAddress =
120136
options.factoryAddress ??
121137
getDefaultAccountFactory(options.overrides?.entrypointAddress);
@@ -196,12 +212,24 @@ export async function disconnectSmartWallet(
196212
async function createSmartAccount(
197213
options: SmartAccountOptions,
198214
): Promise<Account> {
215+
const erc20Paymaster = options.overrides?.tokenPaymaster;
216+
if (erc20Paymaster) {
217+
if (
218+
getEntryPointVersion(
219+
options.overrides?.entrypointAddress || ENTRYPOINT_ADDRESS_v0_6,
220+
) !== "v0.7"
221+
) {
222+
throw new Error(
223+
"Token paymaster is only supported for entrypoint version v0.7",
224+
);
225+
}
226+
}
227+
199228
const { accountContract } = options;
200229
const account: Account = {
201230
address: getAddress(accountContract.address),
202231
async sendTransaction(transaction: SendTransactionOption) {
203232
// if erc20 paymaster - check allowance and approve if needed
204-
const erc20Paymaster = options.overrides?.erc20Paymaster;
205233
let paymasterOverride:
206234
| undefined
207235
| ((
@@ -215,12 +243,7 @@ async function createSmartAccount(
215243
});
216244
const paymasterCallback = async (): Promise<PaymasterResult> => {
217245
return {
218-
paymasterAndData: concatHex([
219-
erc20Paymaster.address as Hex,
220-
erc20Paymaster?.token as Hex,
221-
]),
222-
// for 0.7 compatibility
223-
paymaster: erc20Paymaster.address as Hex,
246+
paymaster: erc20Paymaster.paymasterAddress as Hex,
224247
paymasterData: "0x",
225248
};
226249
};
@@ -436,13 +459,10 @@ async function createSmartAccount(
436459
async function approveERC20(args: {
437460
accountContract: ThirdwebContract;
438461
options: SmartAccountOptions;
439-
erc20Paymaster: {
440-
address: string;
441-
token: string;
442-
};
462+
erc20Paymaster: TokenPaymasterConfig;
443463
}) {
444464
const { accountContract, erc20Paymaster, options } = args;
445-
const tokenAddress = erc20Paymaster.token;
465+
const tokenAddress = erc20Paymaster.tokenAddress;
446466
const tokenContract = getContract({
447467
address: tokenAddress,
448468
chain: accountContract.chain,
@@ -451,7 +471,7 @@ async function approveERC20(args: {
451471
const accountAllowance = await allowance({
452472
contract: tokenContract,
453473
owner: accountContract.address,
454-
spender: erc20Paymaster.address,
474+
spender: erc20Paymaster.paymasterAddress,
455475
});
456476

457477
if (accountAllowance > 0n) {
@@ -460,7 +480,7 @@ async function approveERC20(args: {
460480

461481
const approveTx = approve({
462482
contract: tokenContract,
463-
spender: erc20Paymaster.address,
483+
spender: erc20Paymaster.paymasterAddress,
464484
amountWei: maxUint96 - 1n,
465485
});
466486
const transaction = await toSerializableTransaction({
@@ -478,7 +498,7 @@ async function approveERC20(args: {
478498
...options,
479499
overrides: {
480500
...options.overrides,
481-
erc20Paymaster: undefined,
501+
tokenPaymaster: undefined,
482502
},
483503
},
484504
});

packages/thirdweb/src/wallets/smart/lib/bundler.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,26 @@ export async function bundleUserOp(args: {
6767
* ```
6868
* @walletUtils
6969
*/
70-
export async function estimateUserOpGas(args: {
71-
userOp: UserOperationV06 | UserOperationV07;
72-
options: BundlerOptions;
73-
}): Promise<EstimationResult> {
70+
export async function estimateUserOpGas(
71+
args: {
72+
userOp: UserOperationV06 | UserOperationV07;
73+
options: BundlerOptions;
74+
},
75+
stateOverrides?: {
76+
[x: string]: {
77+
stateDiff: {
78+
[x: string]: `0x${string}`;
79+
};
80+
};
81+
},
82+
): Promise<EstimationResult> {
7483
const res = await sendBundlerRequest({
7584
...args,
7685
operation: "eth_estimateUserOperationGas",
7786
params: [
7887
hexlifyUserOp(args.userOp),
7988
args.options.entrypointAddress ?? ENTRYPOINT_ADDRESS_v0_6,
89+
stateOverrides,
8090
],
8191
});
8292

packages/thirdweb/src/wallets/smart/lib/constants.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Chain } from "../../../chains/types.js";
22
import { getAddress } from "../../../utils/address.js";
33
import { getThirdwebDomains } from "../../../utils/domains.js";
4+
import type { TokenPaymasterConfig } from "../types.js";
45

56
export const DUMMY_SIGNATURE =
67
"0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c";
@@ -17,6 +18,28 @@ export const ENTRYPOINT_ADDRESS_v0_7 =
1718

1819
export const MANAGED_ACCOUNT_GAS_BUFFER = 50000n;
1920

21+
type PAYMASTERS = "BASE_USDC" | "CELO_CUSD" | "LISK_LSK";
22+
export const TokenPaymaster: Record<PAYMASTERS, TokenPaymasterConfig> = {
23+
BASE_USDC: {
24+
chainId: 8453,
25+
paymasterAddress: "0x2222f2738BE6bB7aA0Bfe4AEeAf2908172CF5539",
26+
tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
27+
balanceStorageSlot: 9n,
28+
},
29+
CELO_CUSD: {
30+
chainId: 42220,
31+
paymasterAddress: "0x3feA3c5744D715ff46e91C4e5C9a94426DfF2aF9",
32+
tokenAddress: "0x765DE816845861e75A25fCA122bb6898B8B1282a",
33+
balanceStorageSlot: 9n,
34+
},
35+
LISK_LSK: {
36+
chainId: 1135,
37+
paymasterAddress: "0x9eb8cf7fBa5ed9EeDCC97a0d52254cc0e9B1AC25",
38+
tokenAddress: "0xac485391EB2d7D88253a7F1eF18C37f4242D1A24",
39+
balanceStorageSlot: 9n,
40+
},
41+
};
42+
2043
/*
2144
* @internal
2245
*/

packages/thirdweb/src/wallets/smart/lib/userop.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { concat } from "viem";
1+
import { maxUint96 } from "ox/Solidity";
2+
import { concat, keccak256, toHex } from "viem";
23
import type { Chain } from "../../../chains/types.js";
34
import type { ThirdwebClient } from "../../../client/client.js";
45
import {
@@ -13,6 +14,7 @@ import { encode } from "../../../transaction/actions/encode.js";
1314
import { toSerializableTransaction } from "../../../transaction/actions/to-serializable-transaction.js";
1415
import type { PreparedTransaction } from "../../../transaction/prepare-transaction.js";
1516
import type { TransactionReceipt } from "../../../transaction/types.js";
17+
import { encodeAbiParameters } from "../../../utils/abi/encodeAbiParameters.js";
1618
import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js";
1719
import type { Hex } from "../../../utils/encoding/hex.js";
1820
import { hexToBytes } from "../../../utils/encoding/to-bytes.js";
@@ -361,17 +363,38 @@ async function populateUserOp_v0_7(args: {
361363
paymasterResult.paymasterVerificationGasLimit;
362364
} else {
363365
// otherwise fallback to bundler for gas limits
364-
const estimates = await estimateUserOpGas({
365-
userOp: partialOp,
366-
options: bundlerOptions,
367-
});
366+
const stateOverrides = overrides?.tokenPaymaster
367+
? {
368+
[overrides.tokenPaymaster.tokenAddress]: {
369+
stateDiff: {
370+
[keccak256(
371+
encodeAbiParameters(
372+
[{ type: "address" }, { type: "uint256" }],
373+
[
374+
accountContract.address,
375+
overrides.tokenPaymaster.balanceStorageSlot,
376+
],
377+
),
378+
)]: toHex(maxUint96, { size: 32 }),
379+
},
380+
},
381+
}
382+
: undefined;
383+
const estimates = await estimateUserOpGas(
384+
{
385+
userOp: partialOp,
386+
options: bundlerOptions,
387+
},
388+
stateOverrides,
389+
);
368390
partialOp.callGasLimit = estimates.callGasLimit;
369391
partialOp.verificationGasLimit = estimates.verificationGasLimit;
370392
partialOp.preVerificationGas = estimates.preVerificationGas;
371-
partialOp.paymasterPostOpGasLimit =
372-
paymasterResult.paymasterPostOpGasLimit || 0n;
393+
partialOp.paymasterPostOpGasLimit = overrides?.tokenPaymaster
394+
? 500000n // TODO: estimate this better, needed if there's an extra swap needed in the paymaster
395+
: estimates.paymasterPostOpGasLimit || 0n;
373396
partialOp.paymasterVerificationGasLimit =
374-
paymasterResult.paymasterVerificationGasLimit || 0n;
397+
estimates.paymasterVerificationGasLimit || 0n;
375398
// need paymaster to re-sign after estimates
376399
const paymasterResult2 = (await getPaymasterAndData({
377400
userOp: partialOp,

0 commit comments

Comments
 (0)