Multicall3を使ってERC-20のapproveとtransferFromを1回のトランザクションで実行する
はじめに
スマートコントラクトでアプリケーションを開発する際、「トークンを受け取ったら何か処理を行う」というユースケースはよくあります。
しかし、ERC-20のtransfer関数だけでは、アプリケーションのコントラクトを起動できません。
そのため、まずユーザーがapproveでトークン移転の許可を与え、その後アプリケーションのコントラクトがtransferFromを使ってトークンを受け取るというフローが必要になります。
従来、(ERC-1363のような実装がない)純粋なERC-20トークンでは、上記フローの1.と2.の操作は別々のトランザクションとして実行する必要がありました。
しかし、EIP-7702の導入により1回のトランザクション送信で実現可能になったので本記事ではその実装方法を紹介します。
対象読者
- Solidityの基礎知識がある方
EIP-7702の概要
EIP-7702は、EOAが一時的にスマートコントラクトのコードを実行できるようにする提案で、2025年5月7日のPectraアップグレードで導入されました。
今回は、このデリゲート先コントラクトにバッチ処理可能なコントラクトを指定することで目的を達成します。
実装
ここからはコードを使って具体的な実装方法を説明します。
アプリケーションコントラクト
まず、アプリケーションコントラクトを定義します。(DAppsが利用するコントラクトという想定です。)
ここでは、アプリケーションとしての処理は省略し、指定したERC-20トークンからtransferFromでトークンを受け取る処理のみを実装します。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract TransferFromExample {
using SafeERC20 for IERC20;
function run(address erc20Address, uint256 amount) external {
IERC20(erc20Address).safeTransferFrom(msg.sender, address(this), amount);
// ここでトークンを受け取った後の処理を実行(省略)
}
}
これをSepoliaテストネットにデプロイしました。コントラクトアドレスは0xAfcC2c61CB9cf4b8383cD458765F5d849D2e4C71です。
バッチ処理コントラクト
次はデリゲート先となるコントラクトです。今回はバッチ処理を実行したいので、Multicall3コントラクトを利用します。
Multicall3の詳細はGitHubリポジトリをご覧ください。
コントラクトアドレスは以下のページで確認できます。Sepoliaテストネットには0xcA11bde05977b3631167028862bE2a173976CA11でデプロイされています。
ERC-20コントラクト
最後はERC-20トークンコントラクトです。こちらも既存のものを使用します。今回はSepoliaテストネットのUSDCトークンを使います。
コントラクトアドレスは0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238です。
USDCトークンは以下のサイトから無料で入手できます。
トランザクション送信スクリプト
必要なコントラクトが揃ったので、実際にapproveとtransferFromを1回のトランザクションで実行するスクリプトを作成していきます。
import { ethers, Wallet, Interface, AddressLike, BytesLike, JsonRpcProvider } from "ethers";
import dotenv from "dotenv";
import { TransferFromExample__factory } from "../typechain-types";
// Multicall3のコントラクトアドレス。EOAのデリゲート先として使用。
const SEPOLIA_MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
// USDCコントラクトアドレス
const SEPOLIA_USDC_ADDRESS = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238";
// 転送するUSDCの量
const USDC_AMOUNT = 10_000n; // 0.01 USDC (USDCのdecimalsは6)
// デプロイしたTransferFromExampleコントラクト(アプリケーションコントラクト)のアドレス
const TRANSFER_FROM_EXAMPLE_ADDRESS = "0xAfcC2c61CB9cf4b8383cD458765F5d849D2e4C71";
(async () => {
// .envファイル読み込み
// - PRIVATE_KEY: EOAアカウントの秘密鍵
// - SEPOLIA_RPC_URL: SepoliaネットワークのRPC URL
dotenv.config();
const runner = new Wallet(process.env.PRIVATE_KEY!, new JsonRpcProvider(process.env.SEPOLIA_RPC_URL!)); // EOAアカウント
const erc20Interface = new Interface([
"function approve(address spender, uint256 amount) external returns (bool)"
]);
const transferFromExampleInterface = TransferFromExample__factory.createInterface();
const approveData = erc20Interface.encodeFunctionData("approve", [TRANSFER_FROM_EXAMPLE_ADDRESS, USDC_AMOUNT]);
const runData = transferFromExampleInterface.encodeFunctionData("run", [SEPOLIA_USDC_ADDRESS, USDC_AMOUNT]);
// Multicall3のaggregate3関数に渡すcallsデータを作成
const calls: { target: AddressLike; allowFailure: boolean; callData: BytesLike; }[] = [
{
target: SEPOLIA_USDC_ADDRESS,
allowFailure: false,
callData: approveData
},
{
target: TRANSFER_FROM_EXAMPLE_ADDRESS,
allowFailure: false,
callData: runData
}
];
// 委譲認可を作成(EIP-7702)
const auth = await runner.authorize({
address: SEPOLIA_MULTICALL3_ADDRESS,
nonce: (await runner.provider!.getTransactionCount(runner.address)!) + 1
});
const multicall3Interface = new Interface([
"function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[] returnData)"
]);
const delegatedContract = new ethers.Contract(
runner.address,
multicall3Interface,
runner
);
// トランザクション送信
const tx = await delegatedContract.aggregate3(calls, {
type: 4,
authorizationList: [auth],
});
console.log("Transaction Hash:", tx.hash);
await tx.wait();
console.log("Transaction confirmed.");
})();
処理フローは以下のようになります。
実際にこのスクリプトを実行したときのトランザクションがこちらです。
トランザクション送信元アドレスは0xA611Fd621C94af6211FFCd115c727c1CAf221D82です。
次の画像ではデリゲート先アドレスがMulticall3コントラクトアドレスになっていることがわかります。
また、EIP-7702で導入されたトランザクションタイプ4が使われていることが記録されていることが確認できます。

こちらの画像では0.01 USDCがアプリケーションコントラクトに転送されていることがわかります。
また、LogsタブではapproveとtransferFromで発生するイベント(ApprovalとTransfer)が1つのトランザクションに含まれていることも確認できます。


なぜMulticall3だけでは実現できなかったのか
Multicall3は複数のコントラクト呼び出しを1回のトランザクションで実行できます。
そのため、EIP-7702導入前でもapproveとtransferFromを1回のトランザクションで実現できそうに見えます。
しかし、単にMulticall3のaggregate3関数を使った場合、approve呼び出し時のmsg.senderはMulticall3コントラクトのアドレスになってしまいます。
そのため、approveの処理は意図しないものとなり、通常は失敗します。
一方、EIP-7702の機能を使うと、Multicall3コントラクトのコードを一時的にEOAのコードに設定して実行するため、msg.senderはEOAのアドレスとなり、意図した通りに動作します。
MetaMaskなどのウォレットでもできるのか
先ほどのコードでは、秘密鍵を使ってethers.jsのWalletオブジェクトを作成し、authorizeメソッドで委譲認可を生成していました。
const runner = new Wallet(process.env.PRIVATE_KEY!, new JsonRpcProvider(process.env.SEPOLIA_RPC_URL!)); // EOAアカウント
...
// 委譲認可を作成(EIP-7702)
const auth = await runner.authorize({
address: SEPOLIA_MULTICALL3_ADDRESS,
nonce: (await runner.provider?.getTransactionCount(runner.address)!) + 1
});
では、MetaMaskなどのウォレットを使う場合はどうでしょうか。
以下のように記述すればよさそうですが、実際に実行しようとするとエラーになります。(2025年10月時点)
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
...
// 委譲認可を作成(EIP-7702)
const auth = await signer.authorize({ // ここでエラーになる
address: SEPOLIA_MULTICALL3_ADDRESS,
nonce: (await provider.getTransactionCount(signer.getAddress())) + 1
});
# ethers.js v6でのエラー表示
authorization not implemented for this signer (operation="authorize", code=UNSUPPORTED_OPERATION, version=6.15.0)
EIPの記述によると、ウォレットは委譲認可を作成のUIを提供すべきでないとあるので、今後も上記コードが動作することは無いと考えるのが妥当でしょう。
While this EIP provides a lot of flexibility to applications and EOAs, there are incorrect ways of using it. Applications must not expect that they can suggest the user sign an authorization, and therefore it is the duty of the wallet to not provide an interface to do so.
There is no safe way to provide this interface. The code specified by an authorization has unrestricted access to the account and must always be closely audited by the wallet. Few users have the level of sophistication to reasonably verify the code they are delegating to.
ChatGPTによる和訳
このEIPはアプリケーションやEOAに多くの柔軟性を提供しますが、誤った使用方法も存在します。
アプリケーションは「ユーザーに署名付き認可を求めることができる」と想定してはなりません。したがって、そのような操作を行うインターフェースを提供しないようにすることはウォレットの責務です。
このインターフェースを安全に提供する方法は存在しません。
認可によって指定されたコードはアカウントへの無制限なアクセス権を持つため、ウォレットによって常に厳密に監査されなければなりません。
自分が委任しようとしているコードを適切に検証できるだけの知識を持つユーザーはほとんどいません。
これらから、MetaMaskのようなウォレットではEIP-7702の機能を使ってapproveとtransferFromを1回のトランザクションで実行できないように見えますが、別の方法で実現可能です。
その方法については、また別の記事で紹介します。
Discussion