Частые ошибки при переходе на Account Abstraction
Разбираем типичные ошибки при внедрении ERC-4337: неверная оценка газа UserOperation, путаница initCode, проблемы с nonce у смарт-аккаунтов.
Где чаще всего ломается внедрение ERC-4337
Переход с EOA на Account Abstraction редко падает на «непоняли идею». Обычно ломается три места: газ UserOperation, деплой через initCode/factory и nonce smart-аккаунта.
Ниже — разбор с диагностикой в коде. Путь UserOp → Bundler → EntryPoint — в статье про ERC-4337.
Неверная оценка газа UserOperation
У EOA часто хватает estimateGas + запас. У UserOp три отдельных лимита:
| Поле | За что отвечает |
|---|---|
verificationGasLimit | validateUserOp, проверка подписи, иногда деплой |
callGasLimit | Исполнение callData на аккаунте |
preVerificationGas | Compensация bundler'у за calldata / pre-checks |
Типичный провал: скопировать gasLimit: 21000 с EOA-transfer или занизить verificationGasLimit. Bundler отклоняет UserOp на симуляции (AA21, AA23, out of gas на validation) ещё до включения в блок.
Всегда сначала eth_estimateUserOperationGas, потом подставляете лимиты и только после этого подписываете и шлёте eth_sendUserOperation.
import { ethers } from "ethers";
const ENTRY_POINT_V07 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
const bundlerRpc = process.env.BUNDLER_RPC;
if (!bundlerRpc) {
throw new Error("Задайте BUNDLER_RPC (Pimlico / Alchemy AA endpoint)");
}
const bundler = new ethers.JsonRpcProvider(bundlerRpc);
// Минимальный каркас UserOp v0.7 до оценки газа.
// signature: «dummy» нужен многим bundler'ам для estimate (длина как у реальной ECDSA).
const userOperation = {
sender: process.env.SMART_ACCOUNT_ADDRESS as string,
nonce: "0x0",
callData: "0x",
callGasLimit: "0x0",
verificationGasLimit: "0x0",
preVerificationGas: "0x0",
maxFeePerGas: ethers.toBeHex(ethers.parseUnits("1", "gwei")),
maxPriorityFeePerGas: ethers.toBeHex(ethers.parseUnits("0.1", "gwei")),
signature:
"0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c",
};
if (!userOperation.sender || !ethers.isAddress(userOperation.sender)) {
throw new Error("Задайте SMART_ACCOUNT_ADDRESS");
}
const gas = await bundler.send("eth_estimateUserOperationGas", [
userOperation,
ENTRY_POINT_V07,
]);
console.log("preVerificationGas:", gas.preVerificationGas);
console.log("verificationGasLimit:", gas.verificationGasLimit);
console.log("callGasLimit:", gas.callGasLimit);
// Подставьте gas.* в UserOp, затем реальную signature — и только потом eth_sendUserOperationЕсли estimate падает, а send «как-нибудь» проходит с огромными лимитами — вы маскируете баг валидации или callData. Чините причину, не раздувайте gas вслепую.
Путаница initCode и factory
В EntryPoint v0.6 первый деплой шёл через одно поле initCode (= factory || factoryData).
В v0.7 то же самое разбито на factory + factoryData. Bundler ждёт unpacked-формат; на EntryPoint он сам упакует.
| Ошибка | Симптом |
|---|---|
Шлёте initCode на EntryPoint v0.7 / bundler v0.7 | Reject / неизвестное поле |
factory+factoryData при уже задеплоенном sender | Лишний деплой или revert |
Пустые factory-поля, а getCode(sender) === "0x" | Аккаунт не существует → validation fail |
sender ≠ CREATE2-адрес от factory+данных | «Sender address mismatch» / AA14 |
Правило: перед сборкой UserOp проверьте bytecode.
import { ethers } from "ethers";
const eth = new ethers.JsonRpcProvider("https://ethereum.publicnode.com");
const sender = process.env.SMART_ACCOUNT_ADDRESS as string;
const code = await eth.getCode(sender);
const deployed = code !== "0x";
console.log("sender:", sender);
console.log("deployed:", deployed);
if (!deployed) {
console.log(
"Нужны factory + factoryData (v0.7) или initCode (v0.6) — иначе EntryPoint не найдёт аккаунт",
);
} else {
console.log("factory/factoryData не передавайте — аккаунт уже on-chain");
}Адрес counterfactual-аккаунта должен совпадать с тем, что вернёт factory (getAddress / createAccount preview в SDK). Ручной sender «на глаз» — частая причина ночных дебагов.
Проблемы с nonce у smart-аккаунтов
Nonce EOA — счётчик обычных транзакций: provider.getTransactionCount(address).
У ERC-4337 nonce другой: его хранит EntryPoint (схема key + sequence). getTransactionCount(smartAccount) для UserOp не подходит.
import { ethers } from "ethers";
const ENTRY_POINT_V07 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
const eth = new ethers.JsonRpcProvider("https://ethereum.publicnode.com");
const entryPoint = new ethers.Contract(
ENTRY_POINT_V07,
["function getNonce(address sender, uint192 key) view returns (uint256)"],
eth,
);
const sender = process.env.SMART_ACCOUNT_ADDRESS as string;
if (!sender) throw new Error("Задайте SMART_ACCOUNT_ADDRESS");
const eoaStyleNonce = await eth.getTransactionCount(sender);
const aaNonceKey0 = await entryPoint.getNonce(sender, 0);
const aaNonceKey1 = await entryPoint.getNonce(sender, 1);
console.log("getTransactionCount (не для UserOp):", eoaStyleNonce);
console.log("EntryPoint nonce key=0:", aaNonceKey0.toString());
console.log("EntryPoint nonce key=1:", aaNonceKey1.toString());Типичные провалы:
- Подставить
0всегда → после первого UserOp bundler отвечает AA25 / nonce error. - Параллельные UserOp с одним и тем же key+sequence → один пройдёт, остальные отвалятся (как у EOA с одним nonce, но источник другой).
- Путать nonce bundler EOA (обычный TX к EntryPoint) с nonce sender в UserOp — это вообще разные аккаунты.
Для параллельных потоков используйте разные nonce key (2D nonce), если аккаунт и SDK это поддерживают — не крутите один sequence вручную с гонками.
Как это работает
eth_estimateUserOperationGas — симуляция на стороне bundler: validation + call. Результат привязан к текущему callData, signature-заглушке и состоянию сети; после смены callData оценку повторяют.
factory / factoryData срабатывают только если sender ещё без кода. EntryPoint деплоит аккаунт в verification-фазе, поэтому verification gas на первом UserOp заметно выше.
EntryPoint.getNonce(sender, key) — канонический источник nonce для UserOp. SDK (permissionless, Account Kit, ZeroDev) обычно подставляют его сами; ручная сборка UserOp без getNonce почти всегда ошибочна.
Чеклист перед eth_sendUserOperation
Газ «с потолка» → сначала eth_estimateUserOperationGas, потом подпись.
v0.6-поля на v0.7 EntryPoint → initCode / paymasterAndData замените на factory+factoryData и отдельные paymaster*.
getTransactionCount для UserOp → берите EntryPoint.getNonce.
Нет ETH на аккаунте и нет Paymaster → bundler не включит UserOp; нужен депозит в EntryPoint или спонсор.
Путать userOpHash и hash бандла → статус через eth_getUserOperationReceipt, не только getTransactionReceipt.
Что дальше
- Paymaster: как оплатить газ за пользователя — если ошибка про insufficient funds / prepaid gas
- Обзор провайдеров Account Abstraction — Safe, ZeroDev, Alchemy Account Kit
- Что такое nonce и зачем он нужен — EOA-nonce; не путать с nonce EntryPoint