2025.04.09
ERC20トークンでガス代を支払う
こんにちは。次世代システム研究室のL.W.です。
「イーサリアムでのアカウントの抽象化(Account Abstraction)の紹介」でAA(Account Abstraction)を紹介しました。AAを活用できれば、EOAのように秘密鍵を保管する必要なくなります。ETHのガス代も他人によりの代払いが可能となります。ユーザの利便性の向上に繋がります。
WorldなどAAを活用するいろんな事例が出てきますが、今回はERC20トークンでガス代を支払うことを紹介します。
イーサリアムとArbitrumを始めのほとんどのL2ブロックチェーンには、ETHでガスを支払うのはプロトコルで決められたことで、プロトコルのアップグレードがないと、これは変わらないでしょう。ですから、Account Abstractionでは、paymasterがユーザの代わりに、ETHでネットワークのガス代を支払いますが、paymasterの運営側はユーザからERC20トークンを徴収するか、ユーザをスポンサーするか、などの方式があります。
1.Paymasterがユーザをスポンサーする
出典:https://usa.visa.com/solutions/crypto/rethink-digital-transactions-with-account-abstraction.html
Paymasterはどのようなユーザからのトランザクションをスポンサーするか、先の何件トランザクションをスポンサーするか、などのルールを柔軟に設定することができます。
詳しい例として、Alchemy PaymasterのGas Managerに参照できます。
2.PaymasterがユーザからERC20トークンを徴収する
出典:https://usa.visa.com/solutions/crypto/rethink-digital-transactions-with-account-abstraction.html
Paymaster側は自分のコントラクトの実装により、サポートするERC20トークンが異なります。オラクルを通して、ETHとERC20トークンのレートを取得し、肩代わりしたガス代の分以外に、一定の手数料を徴収するのはほとんです。
3.Paymasterの市場
Paymasterサービスを提供するプロバイダーが多く存在していますが、ユーザの利便性向上、手数料の求めなどのため、新しいプレイヤーもどんどん出てきています。今年1月23日、circleもPaymasterのテービス提供開始の発表をしたばかりで、話題になっています。
3.1. ERC-4337全体図
出典:https://learnblockchain.cn/article/6925
ERC-4337でAAの各コンポーネントを定義されています。
各コンポーネントに取り組んでいるプロジェクトが多く存在しています。ユーザの選択肢が増える一方で、選定と組み合わせには工夫が必要です。
3.2. アプリケーション
ERC-4337をサポートするウォレットやDappがどんどん増えています。
Worldウォレットの場合、WLDトークンの送金にはETHがかかりますが、スポンサーされています。
詳しくは、ここに参照できます。
Safe4337Moduleは一番使われているように見えます。
3.3. Smart Contract Wallet
Account Factoryと呼ばれることもあります。どのようなsmart contract walletがアプリケーションにより採用されたか、ここで参照することができます。
実装により、認証方法、Multiple Signersサポート、ERC-7579サポートが異なります。
3.4. Bundler
実際のトランザクション手数料の何パーセントを自分の利益とする狙いですが、ユーザの囲い込む目的で今は徴収しないbundlerプロバイダーもあります。
詳しくはここに参照できます。
Bundlerの運営にはリスクがあります。マイナスの利益が出てくることがあります。
今はAlchemy、coinbase、pimlicoはメインプレイヤーとなっています。
3.5. Paymaster
主なPaymasterのプロバイダーは以下の通りです。詳しくはここに参照できます。
3.5.1 pimlico paymaster
- 手数料:10%
-
const gasPrice = min(userop.maxFeePerGas, baseFee + userop.maxPriorityFeePerGas); const actualGasCost = actualGas * gasPrice; const billedAmount = actualGasCost * 1.1; // 10% surcharge
-
- サポートするブロックチェーンとトークン
- ブロックチェーン
- https://docs.pimlico.io/infra/paymaster/erc20-paymaster/supported-tokens
- トークン
- まだ少ないです。(USDC、USDT、sETH、wsETH)
- ブロックチェーン
- Paymaster Contract Address
- 各ブロックチェーンでは同じアドレスのコントラクトをデプロイしました。
- v0.7:0x0000000000000039cd5e8ae05257ce51c473ddd1
v0.6:0x00000000000000fb866daaa79352cc568a005d96
- v0.7:0x0000000000000039cd5e8ae05257ce51c473ddd1
- 各ブロックチェーンでは同じアドレスのコントラクトをデプロイしました。
- 実例
- このトランザクションでは、0.000180120878828854 ETHのガス代ですが、0.439647USDCを徴収したことが分かりました。
- デモ
-
import { toSimpleSmartAccount } from "permissionless/accounts" import { createPimlicoClient } from "permissionless/clients/pimlico" import { createPublicClient, getAddress, type Hex, http, parseAbi } from "viem" import { createBundlerClient, entryPoint07Address, UserOperation, type EntryPointVersion, } from "viem/account-abstraction" import { privateKeyToAccount } from "viem/accounts" import { baseSepolia } from "viem/chains" //pim_CkAn6RrW6oVP9Kpp9Rpbvu const pimlicoUrl = `https://api.pimlico.io/v2/${baseSepolia.id}/rpc?apikey=${process.env.PIMLICO_API_KEY}` const publicClient = createPublicClient({ chain: baseSepolia, transport: http("https://sepolia.base.org"), }) const pimlicoClient = createPimlicoClient({ chain: baseSepolia, transport: http(pimlicoUrl), entryPoint: { address: entryPoint07Address, version: "0.7" as EntryPointVersion, }, }) const bundlerClient = createBundlerClient({ account: await toSimpleSmartAccount({ client: publicClient, owner: privateKeyToAccount(process.env.PRIVATE_KEY as Hex), }), chain: baseSepolia, transport: http(pimlicoUrl), paymaster: pimlicoClient, userOperation: { estimateFeesPerGas: async () => { return (await pimlicoClient.getUserOperationGasPrice()).fast }, }, }) const usdc = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" const smartAccountAddress = bundlerClient.account.address const senderUsdcBalance = await publicClient.readContract({ abi: parseAbi(["function balanceOf(address account) returns (uint256)"]), address: usdc, functionName: "balanceOf", args: [smartAccountAddress], }) if (senderUsdcBalance < 1_000_000n) { throw new Error("insufficient USDC balance, required at least 1 USDC.") } const quotes = await pimlicoClient.getTokenQuotes({ tokens: [usdc], }) const { postOpGas, exchangeRate, paymaster } = quotes[0] const userOperation: UserOperation<"0.7"> = await bundlerClient.prepareUserOperation({ calls: [ { to: getAddress("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"), data: "0x1234" as Hex, }, ], }) const userOperationMaxGas = userOperation.preVerificationGas + userOperation.callGasLimit + userOperation.verificationGasLimit + (userOperation.paymasterPostOpGasLimit || 0n) + (userOperation.paymasterVerificationGasLimit || 0n) const userOperationMaxCost = userOperationMaxGas * userOperation.maxFeePerGas // using formula here https://github.com/pimlicolabs/singleton-paymaster/blob/main/src/base/BaseSingletonPaymaster.sol#L334-L341 const maxCostInToken = ((userOperationMaxCost + postOpGas * userOperation.maxFeePerGas) * exchangeRate) / BigInt(1e18) const hash = await bundlerClient.sendUserOperation({ paymasterContext: { token: usdc, }, calls: [ { abi: parseAbi(["function approve(address,uint)"]), functionName: "approve", args: [paymaster, maxCostInToken], to: usdc, }, { to: getAddress("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"), data: "0x1234" as Hex, }, ], }) const opReceipt = await bundlerClient.waitForUserOperationReceipt({ hash, }) console.log(`transactionHash: ${opReceipt.receipt.transactionHash}`)
-
3.5.2 biconomy paymaster
- 手数料
- paymasterRulesにより、変わります。
- サポートするブロックチェーンとトークン
- ブロックチェーンにより、サポートするトークンも異なります。
- https://docs.biconomy.io/infra/paymaster/supportedTokens
- Paymaster Contract Address
- BaseとOptimismの場合
- Sponsorship Paymaster :0x0000006087310897e0BFfcb3f0Ed3704f7146852
ERC20 Token Paymaster :0x00000000301515A5410e0d768aF4f53c416edf19
- Sponsorship Paymaster :0x0000006087310897e0BFfcb3f0Ed3704f7146852
- 他のブロックチェーンの場合
- Sponsorship Paymaster :0x00000072a5F551D6E80b2f6ad4fB256A27841Bbc
ERC20 Token Paymaster :0x00000000301515A5410e0d768aF4f53c416edf19
- Sponsorship Paymaster :0x00000072a5F551D6E80b2f6ad4fB256A27841Bbc
- BaseとOptimismの場合
- 実例
- このトランザクションでは、0.000206919952217238 ETHのガス代ですが、0.595547USDCを徴収したことが分かりました。
- デモ
-
import { createSmartAccountClient, createBicoPaymasterClient, toBiconomyTokenPaymasterContext, toNexusAccount } from "@biconomy/abstractjs"; import { createPublicClient, http, getContract, erc20Abi, formatUnits, maxUint256, parseErc6492Signature, encodePacked, hexToBigInt, encodeFunctionData, parseAbi } from 'viem'; import { baseSepolia } from 'viem/chains'; import { createBundlerClient, toCoinbaseSmartAccount} from 'viem/account-abstraction'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { toEcdsaKernelSmartAccount, toKernelSmartAccount, toSafeSmartAccount } from 'permissionless/accounts'; import { eip2612Abi, eip2612Permit } from './permit-helpers.js'; import fs from 'node:fs'; const BASE_SEPOLIA_BUNDLER = `https://public.pimlico.io/v2/${baseSepolia.id}/rpc`; //const BASE_SEPOLIA_BUNDLER = 'https://bundler.biconomy.io/api/v3/84532/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44'; const BASE_SEPOLIA_USDC = '0x036CbD53842c5426634e7929541eC2318f3dCF7e'; const baseSepoliaUSDC = "0x036cbd53842c5426634e7929541ec2318f3dcf7e"; const bundlerUrl = "https://bundler.biconomy.io/api/v3/84532/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44"; const paymasterUrl = "https://paymaster.biconomy.io/api/v2/84532/F7wyL1clz.75a64804-3e97-41fa-ba1e-33e98c2cc703"; const owner = privateKeyToAccount( fs.existsSync('.owner_private_key') ? fs.readFileSync('.owner_private_key', 'utf8') : (() => { const privateKey = generatePrivateKey(); fs.writeFileSync('.owner_private_key', privateKey); return privateKey; })() ); const account1 = await toNexusAccount({ signer: owner, chain: baseSepolia, transport: http(), }); console.log('Smart wallet address1:', account1.address); const client = createPublicClient({ chain: baseSepolia, transport: http() }); const account = await toKernelSmartAccount({ client, owners: [owner], version: '0.3.1' }); console.log('Owner address:', owner.address); console.log('Smart wallet address:', account.address); const usdc = getContract({ client, address: BASE_SEPOLIA_USDC, abi: [...erc20Abi, ...eip2612Abi] }); const usdcBalance = await usdc.read.balanceOf([account.address]); if (usdcBalance === 0n) { console.log( 'Visit https://faucet.circle.com/ to fund the smart wallet address above ' + '(not the owner address) with some USDC on BASE Sepolia, ' + 'then return here and run the script again.' ); process.exit(); } else { console.log(`Smart wallet has ${formatUnits(usdcBalance, 6)} USDC`); } const paymasterContext = toBiconomyTokenPaymasterContext({ feeTokenAddress: baseSepoliaUSDC }) const nexusClient = createSmartAccountClient({ account, transport: http(bundlerUrl), paymaster: createBicoPaymasterClient({paymasterUrl}), paymasterContext }); function sendUSDC(to, amount) { return { to: usdc.address, abi: usdc.abi, functionName: 'transfer', args: [to, amount] }; } const recipient1 = '0xE6b48d76Bc4805ABF61F38A55F1D7C362c8BfdA8'; const calls = [sendUSDC(recipient1, 1000000n), sendUSDC(recipient1, 2000000n)]; // $0.01 USDC const userOpHash = await nexusClient.sendTokenPaymasterUserOp({ calls, feeTokenAddress: baseSepoliaUSDC }) const receipt = await nexusClient.waitForUserOperationReceipt({ hash: userOpHash }); console.log('\n transaction receipt:', receipt); process.exit();
-
3.5.3 circle paymaster
- 手数料:10%
- キャンペーンで2025/6/30まで手数料なし
- サポートするブロックチェーンとトークン
- 今はBaseとArbitrumしかサポートしていないです。USDCしかサポートしていないです。
- Paymaster Contract Address
- 0x6C973eBe80dCD8660841D4356bf15c32460271C9
- 実例
- このトランザクションでは、0.00001143309 ETHのガス代ですが、0.031249USDCを徴収したことが分かりました。
- デモ
- 特徴は、USDCのeip2612Permit機能を活用で、circle Paymasterへの許可転送金額を事前に設定しおくことができます。
-
import { createPublicClient, http, getContract, erc20Abi, formatUnits, maxUint256, parseErc6492Signature, encodePacked, hexToBigInt, encodeFunctionData, parseAbi } from 'viem'; import { baseSepolia } from 'viem/chains'; import { createBundlerClient, toCoinbaseSmartAccount} from 'viem/account-abstraction'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { toEcdsaKernelSmartAccount, toKernelSmartAccount, toSafeSmartAccount } from 'permissionless/accounts'; import { eip2612Abi, eip2612Permit } from './permit-helpers.js'; import fs from 'node:fs'; const BASE_SEPOLIA_BUNDLER = `https://public.pimlico.io/v2/${baseSepolia.id}/rpc`; const BASE_SEPOLIA_USDC = '0x036CbD53842c5426634e7929541eC2318f3dCF7e'; const BASE_SEPOLIA_PAYMASTER = '0x31BE08D380A21fc740883c0BC434FcFc88740b58'; const MAX_GAS_USDC = 1000000n; // 1 USDC const client = createPublicClient({ chain: baseSepolia, transport: http() }); const block = await client.getBlockNumber(); console.log('Connected to network, latest block is', block); const bundlerClient = createBundlerClient({ client, transport: http(BASE_SEPOLIA_BUNDLER) }); const owner = privateKeyToAccount( fs.existsSync('.owner_private_key') ? fs.readFileSync('.owner_private_key', 'utf8') : (() => { const privateKey = generatePrivateKey(); fs.writeFileSync('.owner_private_key', privateKey); return privateKey; })() ); const account = await toKernelSmartAccount({ client, owners: [owner], version: '0.3.1' }); console.log('Owner address:', owner.address); console.log('Smart wallet address:', account.address); const usdc = getContract({ client, address: BASE_SEPOLIA_USDC, abi: [...erc20Abi, ...eip2612Abi] }); const usdcBalance = await usdc.read.balanceOf([account.address]); if (usdcBalance === 0n) { console.log( 'Visit https://faucet.circle.com/ to fund the smart wallet address above ' + '(not the owner address) with some USDC on BASE Sepolia, ' + 'then return here and run the script again.' ); process.exit(); } else { console.log(`Smart wallet has ${formatUnits(usdcBalance, 6)} USDC`); } const permitData = await eip2612Permit({ token: usdc, chain: baseSepolia, ownerAddress: account.address, spenderAddress: BASE_SEPOLIA_PAYMASTER, value: MAX_GAS_USDC }); const wrappedPermitSignature = await account.signTypedData(permitData); const { signature: permitSignature } = parseErc6492Signature( wrappedPermitSignature ); function sendUSDC(to, amount) { return { to: usdc.address, abi: usdc.abi, functionName: 'transfer', args: [to, amount] }; } //const recipient = privateKeyToAccount(generatePrivateKey()).address; const recipient1 = '0xE6b48d76Bc4805ABF61F38A55F1D7C362c8BfdA8'; const calls = [sendUSDC(recipient1, 1000000n)]; // $1 USDC const recipient2 = '0x5bFFdf61660f67f686ee4bbf726717076bdB8399'; calls.push(sendUSDC(recipient2, 100000n)); // $0.1 USDC const paymaster = BASE_SEPOLIA_PAYMASTER; const paymasterData = encodePacked( ['uint8', 'address', 'uint256', 'bytes'], [ 0n, // Reserved for future use usdc.address, // Token address MAX_GAS_USDC, // Max spendable gas in USDC permitSignature // EIP-2612 permit signature ] ); const additionalGasCharge = hexToBigInt( ( await client.call({ to: paymaster, data: encodeFunctionData({ abi: parseAbi(['function additionalGasCharge() returns (uint256)']), functionName: 'additionalGasCharge' }) }) ).data ); console.log( 'Additional gas charge (paymasterPostOpGasLimit):', additionalGasCharge ); const { standard: fees } = await bundlerClient.request({ method: 'pimlico_getUserOperationGasPrice' }); const maxFeePerGas = hexToBigInt(fees.maxFeePerGas); const maxPriorityFeePerGas = hexToBigInt(fees.maxPriorityFeePerGas); console.log('Max fee per gas:', maxFeePerGas); console.log('Max priority fee per gas:', maxPriorityFeePerGas); console.log('Estimating user op gas limits...'); const { callGasLimit, preVerificationGas, verificationGasLimit, paymasterPostOpGasLimit, paymasterVerificationGasLimit } = await bundlerClient.estimateUserOperationGas({ account, calls, paymaster, paymasterData, // Make sure to pass in the `additionalGasCharge` from the paymaster paymasterPostOpGasLimit: additionalGasCharge, // Use very low gas fees for estimation to ensure successful permit/transfer, // since the bundler will simulate the user op with very high gas limits maxFeePerGas: 1n, maxPriorityFeePerGas: 1n }); const userOpHash = await bundlerClient.sendUserOperation({ account, calls, callGasLimit, preVerificationGas, verificationGasLimit, paymaster, paymasterData, paymasterVerificationGasLimit, // Make sure that `paymasterPostOpGasLimit` is always at least // `additionalGasCharge`, regardless of what the bundler estimated. paymasterPostOpGasLimit: Math.max( Number(paymasterPostOpGasLimit), Number(additionalGasCharge) ), maxFeePerGas, maxPriorityFeePerGas, //paymaster: true, }); console.log('Submitted user op:', userOpHash); console.log('\nWaiting for execution...'); const userOpReceipt = await bundlerClient.waitForUserOperationReceipt({ hash: userOpHash }); const usdcBalanceAfter = await usdc.read.balanceOf([account.address]); const usdcConsumed = usdcBalance - usdcBalanceAfter - 1000000n; // Exclude what we sent console.log('\n USDC paid:', formatUnits(usdcConsumed, 6)); // We need to manually exit the process, since viem leaves some promises on the // event loop for features we're not using. process.exit();
3.5.4 他
- alchemy paymaster
- スポンサー方式がメインで、ERC20トークンがまだサポートしていないようです。
- coinbase paymaster
- 7%の手数料を徴収します。Coinbase Developer Platform’s (CDP) Paymasterでサービスを提供しています。
- stackup paymaster
- 多くのERC20トークンをサポートされています。ProfessionalプランとEnterpriseのプランがあるが、Enterpriseのプランでガス代としてのERC20トークンをカスタマイズできます。
3.5. Entry Point
既にAuditされている、かつデプロイ済みのスマートコントラクトです。インフラの中核になっています。常に進化しています。今の最新バージョンは0.7になっています。
Entry Point 0.7.0 : 0x0000000071727De22E5E9d8BAf0edAc6f37da032
Entry Point 0.6.0 : 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789
各PaymasterはEntry PointでETHをデポジットしておく必要があります。残高が足りない場合、トランザクションの失敗に繋がります。
4.まとめ
Paymasterはユーザのために、肩代わりにETHを支払うことで、ユーザの利便性をもたらすことで、ブロックチェーンのマスアダプションに一歩を近づいています。ERC20トークンでガス代を支払うためには、Paymasterに資金転送の許可(approve)が欠かせないが、これはリスクが秘めています。USDCのようにeip2612Permitを活用で、必要な部分だけを許可することで、リスクを軽減できます。
5.最後に
次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。
皆さんのご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD