Разработка системы batch-транзакций
Пользователь хочет добавить ликвидность в пул Uniswap V3. По факту это: approve токена A → approve токена B → mint позиции. Три транзакции, три подтверждения в MetaMask, три оплаты газа. Если между approve и mint произойдёт фронтран — позиция создастся по нежелательной цене. Batch-транзакции решают это: одно подтверждение, один газ, атомарное исполнение.
Два подхода к batch: router и EIP-4337
Router-паттерн — контракт-агрегатор, который принимает массив вызовов и исполняет их последовательно. Самый простой вариант — Multicall3 от MakerDAO, задеплоенный на большинстве EVM-сетей по адресу 0xcA11bde05977b3631167028862bE2a173976CA11.
struct Call3 {
address target;
bool allowFailure;
bytes callData;
}
function aggregate3(Call3[] calldata calls)
external
payable
returns (Result[] memory returnData);
allowFailure: false делает весь batch атомарным — если один вызов reverts, откатывается всё. allowFailure: true позволяет продолжить batch при ошибке отдельного вызова — используем когда нужна partial execution.
Проблема router-паттерна: approve токена на адрес контракта-роутера. Пользователь должен доверять роутеру, что тот не уведёт токены. Для кастомных роутеров это создаёт UX-барьер и требует аудита.
EIP-4337 (Account Abstraction) — другой уровень. Пользователь контролирует smart contract wallet, который может исполнять несколько вызовов в одной UserOperation. Approve + действие атомарны, без промежуточного доверия роутеру. Стек: Biconomy, Safe{Core} AA SDK, ZeroDev.
Выбор зависит от контекста: для протокольного batch-инга — router, для wallet-уровневой автоматизации — EIP-4337.
Экономия газа: считаем честно
Каждая транзакция в EVM стоит минимум 21 000 gas (intrinsic cost). Batch из 5 операций в отдельных транзакциях: 5 × 21 000 = 105 000 gas только на intrinsic. Через Multicall3 — один раз 21 000 + overhead роутера (~2 000 gas) + gas на каждый вызов без intrinsic cost.
| Сценарий | Отдельные транзакции | Batch (Multicall3) | Экономия |
|---|---|---|---|
| 3 ERC-20 transfer | 3 × 65 000 = 195 000 | ~125 000 | ~36% |
| 5 approve + swap | 5 × 46 000 = 230 000 | ~148 000 | ~36% |
| 10 NFT mint | 10 × 120 000 = 1 200 000 | ~650 000 | ~46% |
Реальные числа зависят от логики каждого вызова, но экономия 30-50% на gas intrinsic cost — консервативная оценка.
Кастомная batch-система: когда Multicall3 не хватает
Multicall3 не принимает ETH с распределением по вызовам (только общий msg.value). Не поддерживает callback-и. Не хранит состояние между вызовами в пакете.
Для сложных сценариев пишем кастомный BatchExecutor:
contract BatchExecutor {
struct BatchCall {
address target;
uint256 value;
bytes data;
bool requireSuccess;
}
function executeBatch(BatchCall[] calldata calls)
external
payable
returns (bytes[] memory results)
{
results = new bytes[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory result) = calls[i].target.call{
value: calls[i].value
}(calls[i].data);
if (calls[i].requireSuccess) {
require(success, _getRevertMsg(result));
}
results[i] = result;
}
}
}
Обязательная проверка безопасности: делегировать ли пользователю выбор target адресов? Если контракт принимает произвольные target — атакующий может вызвать произвольный контракт от имени BatchExecutor. Если BatchExecutor держит approve-ы на токены — это drain. Ограничиваем target whitelist-ом или проверяем, что контракт не держит чужих активов.
Интеграция с frontend через wagmi/viem
На клиентской стороне формируем список вызовов и кодируем через viem:
import { encodeFunctionData } from 'viem';
import { multicall3Abi } from './abis';
const calls = [
{
target: tokenAddress,
allowFailure: false,
callData: encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [spenderAddress, amount]
})
},
{
target: protocolAddress,
allowFailure: false,
callData: encodeFunctionData({
abi: protocolAbi,
functionName: 'deposit',
args: [amount]
})
}
];
await walletClient.writeContract({
address: MULTICALL3_ADDRESS,
abi: multicall3Abi,
functionName: 'aggregate3',
args: [calls]
});
Сроки разработки: интеграция Multicall3 в существующий dApp — 1-2 дня. Кастомный BatchExecutor с whitelist логикой и тестами — 3-5 дней.







