Smart-аккаунт vs EOA: в чём разница на практике
Сравниваем smart-аккаунт (ERC-4337) и обычный EOA-кошелёк: подписи, восстановление доступа, батчинг транзакций, стоимость газа.
Два кошелька, одна сеть
EOA — адрес без кода: один ECDSA-ключ подписывает обычную Ethereum-транзакцию (MetaMask, Rabby, Ledger).
Smart-аккаунт (обычно ERC-4337) — контракт-кошелёк: правила подписи, батчинга и оплаты газа задаёт код. Пользователь подписывает UserOperation, в сеть её доставляет Bundler через EntryPoint.
База типов аккаунтов — в EOA vs Contract Account. Здесь — что это меняет в продукте и в коде.
Сравнение на практике
| Критерий | EOA | Smart-аккаунт (ERC-4337) |
|---|---|---|
| Подпись | Только secp256k1 ECDSA | Любая логика аккаунта: ECDSA, мультиподпись, passkey, session keys |
| Восстановление доступа | Новый ключ / seed; старый адрес не «починить» | Social recovery, смена владельца, гильдии опекунов — если заложено в контракт |
| Мультиподпись | Отдельный контракт (Safe и т.п.) поверх EOA-подписантов | Можно встроить в сам аккаунт |
| Батчинг | Одна цель на TX (или свой multicall) | Несколько вызовов в одном callData UserOp |
| Кто платит газ | EOA должен иметь ETH | Депозит аккаунта в EntryPoint и/или Paymaster |
| Онбординг | Расширение / seed / «купи ETH на газ» | Часто email + gasless (с Paymaster / embedded wallet) |
| Стоимость | Дешевле на простой перевод | Дороже verification + EntryPoint; окупается батчингом и UX |
| Кто шлёт в сеть | Сам кошелёк (eth_sendRawTransaction) | Bundler → EntryPoint.handleOps |
Smart-аккаунт — частный случай Contract Account. Не каждый контракт с bytecode — кошелёк ERC-4337.
Одна задача — два пути в коде
Сценарий: перевести ETH. У EOA это одна подписанная TX. У smart-аккаунта — UserOperation с execute в callData (исполнит EntryPoint после валидации).
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://ethereum.publicnode.com");
const ENTRY_POINT_V07 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
const recipient = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
// --- EOA: одна обычная транзакция ---
async function eoaTransfer(privateKey: string) {
const wallet = new ethers.Wallet(privateKey, provider);
const code = await provider.getCode(wallet.address);
if (code !== "0x") throw new Error("Ожидали EOA без bytecode");
// На mainnet это реальная отправка. Для сухого прогона достаточно собрать TX:
const tx = {
to: recipient,
value: ethers.parseEther("0.01"),
maxFeePerGas: ethers.parseUnits("20", "gwei"),
maxPriorityFeePerGas: ethers.parseUnits("1", "gwei"),
gasLimit: 21_000n,
nonce: await provider.getTransactionCount(wallet.address),
chainId: (await provider.getNetwork()).chainId,
type: 2,
};
const raw = await wallet.signTransaction(tx);
console.log("EOA from:", wallet.address);
console.log("EOA raw TX length:", raw.length);
console.log("Газ платит:", wallet.address);
// await provider.broadcastTransaction(raw);
}
// --- Smart-аккаунт: батч approve + вызов в одном UserOp ---
async function smartAccountBatch(smartAccount: string) {
const code = await provider.getCode(smartAccount);
if (code === "0x") throw new Error("Smart-аккаунт должен иметь bytecode");
const iface = new ethers.Interface([
"function executeBatch(address[] targets, uint256[] values, bytes[] datas)",
"function approve(address spender, uint256 amount)",
]);
// Пример: USDC mainnet + «пустой» второй вызов (в проде — swap / deposit)
const usdc = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const spender = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"; // Uniswap V2 Router
const targets = [usdc, recipient];
const values = [0n, 0n];
const datas = [
iface.encodeFunctionData("approve", [spender, 1_000_000n]), // 1 USDC (6 decimals)
"0x",
];
const callData = iface.encodeFunctionData("executeBatch", [
targets,
values,
datas,
]);
const userOperation = {
sender: smartAccount,
nonce: 0n,
callData,
callGasLimit: 350_000n,
verificationGasLimit: 150_000n,
preVerificationGas: 50_000n,
maxFeePerGas: ethers.parseUnits("20", "gwei"),
maxPriorityFeePerGas: ethers.parseUnits("1", "gwei"),
signature: "0x",
};
console.log("Smart sender:", userOperation.sender);
console.log("EntryPoint:", ENTRY_POINT_V07);
console.log("Batch calls:", targets.length);
console.log("UserOp callData:", userOperation.callData);
console.log("Дальше: подпись → eth_sendUserOperation → Bundler → EntryPoint");
}
// Подставьте свой ключ только в локальных тестах / testnet.
const demoKey =
process.env.PRIVATE_KEY ??
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
await eoaTransfer(demoKey);
const smartAccount = process.env.SMART_ACCOUNT_ADDRESS;
if (smartAccount) {
await smartAccountBatch(smartAccount);
} else {
console.log("SMART_ACCOUNT_ADDRESS не задан — блок smart-аккаунта пропущен");
}У EOA approve + swap — обычно две подписи и две TX. У smart-аккаунта оба шага укладываются в один UserOp через executeBatch (имя функции зависит от реализации кошелька: Safe, Kernel, Light Account и т.д.).
Как это работает
getCode(address) === "0x" — быстрый признак EOA. У smart-аккаунта байткод есть всегда (после деплоя).
EOA платит газ с баланса ключа: без ETH сеть не примет TX. Smart-аккаунт может списать депозит в EntryPoint или переложить оплату на Paymaster.
Восстановление у EOA = контроль seed/ключа. Потеряли ключ — адрес «мёртв» для вас. У smart-аккаунта политика восстановления — часть кода: опекуны, таймлок, смена owner — если провайдер/контракт это поддерживает.
Батчинг экономит не только клики: одна verification + один бандл вместо нескольких отдельных TX, хотя сама verification у ERC-4337 дороже простого EOA-transfer.
Что выбрать
Оставляйте EOA, если аудитория уже в MetaMask/Rabby, достаточно одной подписи ECDSA и вас устраивает «у пользователя есть ETH на газ».
Берите smart-аккаунт, если нужны gasless UX, approve+swap в одном подтверждении, social recovery / passkeys / session keys или онбординг без расширения — часто вместе с embedded-кошельком.
Safe «просто как multisig» без UserOp/EntryPoint — всё ещё зависит от того, что какой-то EOA шлёт обычные TX. Полный AA-flow — это ERC-4337 (или стек, который его прячет).
Частые ошибки
Считать любой контракт-кошелёк ERC-4337 → у адреса может быть bytecode (Gnosis Safe до AA), но без UserOperation / EntryPoint это не тот же путь, что в статье про ERC-4337.
Ждать, что smart-аккаунт всегда дешевле → простой перевод часто дороже из‑за verification. Выигрыш — в батчинге, спонсорстве газа и UX, не в цене одной transfer.
Путать владельца ключа и адрес аккаунта → ключ (или passkey) авторизует UserOp; sender в сети — адрес контракта. Балансы и allowance смотрите на sender, не на EOA-подписанта.
Что дальше
- Paymaster: как оплатить газ за пользователя — как убрать ETH на газ из UX
- ERC-4337: как устроена транзакция — UserOperation, Bundler и EntryPoint по шагам
- EOA vs Contract Account — как отличить кошелёк-ключ от адреса с bytecode