티스토리 뷰
ERC-4337: Account Abstraction를 활용한 스마트 지갑 생성(Privy + Pimlico)
g0rani 2026. 8. 20. 17:52ERC-4337: 계정 추상화란?
내가 이더리움 네트워크를 통해 토큰을 보내거나 여러 DeFi 프로토콜과 상호작용하기 위해서는 메타마스크, 팬텀 지갑과 같은 EOA 계정이 필요하다. 실제로 돈을 보내기 위해서는 EOA의 개인키를 가지고 서명해야 한다. 팝업창에 뜨는 서명 버튼이 이러한 과정이다. 서명은 내가 이 지갑의 주인이고 송금 행위를 승인한다라는 의미이다.
그러나 EOA의 경우 개인키 분실 시 복구가 불가하고, 이를 관리하는 것도 모두 개인에게 책임이 있다. 또한 네트워크 이용 수수료를 ETH로 밖에 지불하지 못한다. 크립토에 친숙하지 않은 일반 사용자들의 경우 메타마스크 지갑을 만들고 사용하는 것 행위 자체에 어려움을 느끼기도 한다.
계정 추상화는 사용자가 EOA 없이도 스마트 컨트랙트 자체를 계정으로 이용할 수 있도록 제안한다. 사용자의 자금이 컨트랙트 안에 존재하며, 계정생성 또한 이메일 또는 SNS 계정을 통해 만들 수 있다. ETH로만 지불 가능했던 수수료도 paymaster 개념 도입으로 USDT와 같은 토큰으로 지불 가능하다. 또한 계정 복구 기능을 제공할 수 있다.
ERC-4337에 대한 자세한 동작 및 설명은 다른 글에서 다루도록하겠다. 해당 표준에 대한 이해가 충분함을 가정하고 간단하게 설명 후 스마트 지갑 생성 과정을 설명하겠다.
ERC-4337 핵심요소
UserOperation: 트랜잭션과 유사한 새로운 객체
- 여러 개의 UserOp들이 alternative mempool에 제출됨
- 일반 트랜잭션과 달리 직접 블록에 포함되지 않고 Bundler가 처리
Bundler: UserOperation들을 모아 Bundle을 구성 후, EntryPoint 컨트랙트의 handleOps() 호출하는 트랜잭션을 생성하는 노드
- 이 트랜잭션이 블록에 포함되면서 UserOperation들이 실행됨
- Bundler = 중개자(EOA의 서명 권한(Private Key))를 내장하고 오프체인에서 24시간 돌아가는 백엔드 서버 프로그램(노드))
EntryPoint: UserOp들을 검증하고 실행하는 핵심 컨트랙트
- 검증과 실행을 분리하여 안전하게 처리
- eth-infinitism(ERC-4337 공식 레퍼런스를 만드는 팀)에서 배포함(배포주소)
Paymaster: 사용자를 대신해 가스비를 내주는 계약 → ERC-20 토큰으로 수수료 지불 가능
스마트 지갑 생성 (Privy + Pimlico + kernel)
aa-demo 레포 - chettz/aa-demo
GitHub - chettz/aa-demo
Contribute to chettz/aa-demo development by creating an account on GitHub.
github.com
Privy는 이메일·소셜 로그인 등으로 사용자를 인증하고, 앱 안에 임베디드 지갑(EOA)을 만들어 주는 서비스다. 이 EOA는 서명키(Signer) 역할을 하고, Privy는 그 키로 제어되는 ERC-4337 스마트 지갑(컨트랙트 계정)을 함께 구성한다.



이후 Next.js 프로젝트를 생성한다.
pnpm create next-app@latest aa-demo
필요한 패키지들을 설치
pnpm add @privy-io/react-auth permissionless viem
.env.local 파일 생성 후 privy app-id를 기입한다.
NEXT_PUBLIC_PRIVY_APP_ID=
app-id는 privy dashboard에서 찾을 수 있다

다음으로 이메일로 로그인하여 임베디드 지갑을 생성할 수 있도록 providers/providers.tsx를 생성한다.
PrivyProvider는 로그인 및 임베디드 지갑(EOA) 생성을 담당하고, SmartWalletsProvider는 생성된 EOA를 Signer로 하는 스마트 월렛을 생성한다.
"use client";
import { PrivyProvider } from "@privy-io/react-auth";
import { SmartWalletsProvider } from "@privy-io/react-auth/smart-wallets";
import { sepolia } from "viem/chains";
export default function Providers({ children }: { children: React.ReactNode }) {
return (
<PrivyProvider
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}
config={{
defaultChain: sepolia,
supportedChains: [sepolia],
embeddedWallets: {
ethereum: { createOnLogin: "users-without-wallets" },
},
}}
>
<SmartWalletsProvider>{children}</SmartWalletsProvider>
</PrivyProvider>
);
}
app/layout.tsx를 생성하여 body를 위에서 생성한 Providers로 감싼다.
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Providers from "@/providers/providers";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">
<Providers>{children}</Providers>
</body>
</html>
);
}
다음으로 로그인 화면과 간단하게 이더를 전송할 수 있는 화면 app/page.tsx를 생성한다.
"use client";
import { usePrivy } from "@privy-io/react-auth";
import React, { useState } from "react";
import { useSmartWallets } from "@privy-io/react-auth/smart-wallets";
import { parseEther } from "viem";
import { sepolia } from "viem/chains";
export default function Home() {
const { ready, authenticated, login, logout, user } = usePrivy();
// client가 스마트 월렛
const { client } = useSmartWallets();
const [txHash, setTxHash] = useState<string | null>(null);
if (!ready) return <p>Loading...</p>;
if (!authenticated) {
return <button onClick={login}>Login</button>;
}
// 임베디드 지갑 주소(signer)와 스마트 지갑 주소 가져오기
const smartWallet = user?.linkedAccounts.find((a) => a.type === "smart_wallet");
const eoa = user?.wallet?.address;
async function onSend(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
if (!client) return;
// form에서 입력된 값 가져오기
const form = new FormData(e.currentTarget);
const to = String(form.get("to"));
const amount = String(form.get("amount"));
// 트랜잭션 전송
const hash = await client.sendTransaction({
chain: sepolia,
to : to,
value: parseEther(amount),
});
setTxHash(hash);
}
return (
<main>
<p>Logged in as {user?.email?.address ?? user?.id}</p>
<p>Smart wallet: {smartWallet?.address ?? "creating..."}</p>
<p>Embdded Wallet(Signer): {eoa ?? "none"}</p>
<form onSubmit={onSend}>
<input name="to" placeholder="0x recipient" required />
<input name="amount" placeholder="0.0001" required />
<button type="submit" disabled={!client}>Send</button>
</form>
{txHash && <p>tx: {txHash}</p>}
<button onClick={logout}>Logout</button>
</main>
);
}
이제 로컬로 띄워 privy에서 제공하는 테스트 이메일로 로그인하면, 생성된 임베디드 지갑 주소와 아직 네트워크에 배포되지는 않았지만 스마트 지갑 주소를 확인할 수 있다. 스마트 지갑 계정은 CREATE2 방식으로 생성되므로 배포 이전에 주소를 미리 계산할 수 있다.
privy 테스트 이메일은 아래 그림과 같이 활성화 할 수 있다.(UserManagement → Authentication → Advanced)

로컬 띄우기
pnpm dev
테스트 이메일로 로그인 후 스마트 지갑 주소를 확인할 수 있다.(아직 네트워크에 배포 X)

스마트 지갑에 아직 테스트 이더가 없으므로 sepolia Faucet에서 0.5 테스트 이더를 먼저 받겠다. 이더스캔으로 확인했을 때 잘 들어온것을 확인할 수 있다.

이제 0.001 ETH를 나의 팬텀 지갑으로 보내보겠다.

수신자 지갑 주소와 전송금액을 입력 후, send 버튼을 누르면 순서대로 승인과 트랜잭션 완료 화면이 뜬다.

총 보낸 이더는 0.001ETH이고 지불할 거래 수수료 예측치는 0.000353ETH이다.
이더스캔을 통해 스마트 지갑에서 일어난 트랜잭션을 아래와 같이 확인할 수 있다. 스마트 지갑 계정 생성 후 첫 userOp 제출이므로 먼저 스마트 지갑(컨트랙트)가 생성된다. 이후 0.001ETH가 전송된 것을 확인할 수 있다. 그런데 거래 수수료 지불로 보이는 트랜잭션에서 실제 전송된 Amount를 보면 위의 화면에서 보았던 예측치인 0.000353ETH와 약 2배 정도 차이가 난다. 어떻게 된 것일까?

이제 실행된 트랜잭션 순서대로 따라가며 스마트 지갑 배포는 누가했고, 0.001ETH를 보내겠다는 userOp은 누가 만들었고, 왜 더 많은 수수료가 전송됐고, 그 수수료는 어디로 누구한테갔는지 설명하겠다.
UserOp
먼저 내가 0.001 ETH를 보내겠다는 userOp은 누가 조립했을까? privy 서비스를 이용하고 있으므로 내가 입력한 수신자 주소와 이더 전송 액션을 바탕으로 privy가 조립했다. 완성된 userOp은 bundler로 전송되는 데 이 과정은 오프체인에서 이루어진다. pimlico에서 제공하는 bundler를 사용중이므로 전달받은 useOp의 내용을 dashboard에서 확인할 수 있다.

0.001ETH를 보냈던 userOp 내용은 아래와 같다.
{
"nonce": "0x845adb2c711129d4f3966735ed98a9f09fc4ce5700000000000000000000",
"sender": "0xFF913299e6ebB5Ba9aB44b022F80f9979b369AA1",
"factory": "0xd703aaE79538628d27099B8c4f621bE4CCd142d5",
"callData": "0xe9ae5c530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000343583a9fc69af3c29d4a8426b5c795a32edcf049e00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000",
"paymaster": null,
"signature": "0x75ccc3db8cd683a7d1a31cbcc94ed35c21058123101ef6b8983f6673d3d4ee800c28380ae93e58a802d45191f875c9f3d3408a9ea5a60fd2e324336c340e23b91b",
"factoryData": "0xc5265d5d000000000000000000000000aac5d4240af87249b3f71bc8e4a2cae074a3e4190000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001243c3b752b01845adb2c711129d4f3966735ed98a9f09fc4ce570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000014e1788ce25cc7b7a8c3d40ee73ca5b9f4c6c763280000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"callGasLimit": "0x4623",
"maxFeePerGas": "0x5e6f096f",
"paymasterData": null,
"preVerificationGas": "0xca63",
"maxPriorityFeePerGas": "0x24748c",
"verificationGasLimit": "0x57749",
"paymasterPostOpGasLimit": "0x0",
"paymasterVerificationGasLimit": "0x0"
}
스마트 지갑 배포
sender는 userOp을 만드는 주체로 스마트 지갑 주소이다. 다음으로 factory와 factoryData 필드에 값이 채워져 있는 것을 확인할 수 있다. factory 필드에 들어가있는 값은 zeroDev(kernerl)에서 배포한 Factory 컨트랙트로, 여기서 스마트 지갑(컨트랙트)를 배포한다. EntryPoint가 Factory 컨트랙트의 지갑 생성함수를 호출하는 흐름은 다음과 같다.(다른 스마트 지갑 생성 서비스를 이용할 경우 다를 수 있다.).
EntryPoint
-> FactoryStaker.deployWithFactory(KernelFactory, data, salt)
-> KernelFactory.createAccount(data, salt)
-> CREATE2 (ERC-1967 proxy)
-> 새 Kernel 지갑
호출 흐름을 보면 KernelFactory에서 바로 배포하는게 아니라, FactoryStaker가 이를 호출하는 2단 구조로 되어있다. 왜 2단 구조로 되어있을까?
먼저 ERC-4337에서 userOp은 서로의 저장소를 건드리지 못한다. A의 검증이 B의 슬롯을 바꾸면, alt-mempool에 있던 다른 userOp이 실패할 수도 있기 때문이다. 따라서 userOp의 sender와 연관된 저장소만 볼 수 있다.
factory와 paymaster의 경우도 마찬가지이다. 이 둘은 전역 엔티티로써 수 천개의 userOp이 같은 주소를 호출한다. factory가 자신의 저장소 값을 일부 변경할 경우 시뮬레이션을 통과한 userOp이 무효처리 되고, bundler는 가스를 버리게 될 수 있다. factory가 자신의 저장소를 쓰고 싶다면 EntryPoint에 bundler가 요구하는 일정량의 ETH를 일정기간 동안 stake해야한다. Bundler는 userOp를 사용자로부터 받았을 때, "이 factory가 지금 stake가 필요한가"를 보고, 필요하면 stake를 확인하고, 평판이 나쁘면 버리고, 둘 다 통과하면 멤풀에 넣는다.
factory가 악의적인 행동을 하거나 userOp이 실패하는 등의 DoS를 유발해도, EntryPoint에 맡긴 ETH는 몰수되거나 하지않고 나중에 다시 뺄 수 있다. stake의 목적은 DoS 비용을 비싸게 만드는 것이다.
Privy -> Pimlico
[오프체인]
-> 오프체인 시뮬레이션 (EntryPoint 검증을 미리 돌려 봄)
-> stake 부족 / 저장소 규칙 위반 / 평판 밴이면 거절
-> 통과하면 번들러 멤풀에 보관
[온체인]
-> 묶어서 handleOps 제출
-> EntryPoint가 실제로 validate + 실행
다시 Kernel 2단 구조 얘기로 돌아와서, KernelFactory.createAccount는 내부 호출에서 스마트 지갑의 주인 즉 signer가 누구인지를 ECDSAValidator라는 외부 컨트랙트에 값을 기록하는 부분이 있다. 이 때문에 KernelFactory는 EntryPoint에 ETH를 stake해야하는데 Kernel 지갑 구현체는 업그레이되면서 버전이 계속 바뀐다. 새로운 버전의 스마트 지갑 구현체가 나오면, userOp의 factory값도 바뀌어야 하고 이때마다 새롭게 ETH를 stake해야하는 번거로움이 있다. 이때 stake가 필요한 부분을 담당하는 FactoryStaker로 KernelFactory를 감쌀 수 있다. 이렇게하면 FactoryStaker가 최초로 한번만 EntryPoint에 ETH를 stake하면, 버전별 KernelFactory들은 추가적인 stake없이 sender이외의 다른 저장소를 사용할 수 있다.
userOp의 factory 필드에는 실제로는 FactoryStaker주소가, factorydata에는 KernelFactory주소를 인자로 포함하는 calldata가 들어갔음을 알 수 있다.

이더스캔에서 스마트 지갑 생성 트랜잭션에 보이는 From의 주소는 KernlFactory주소이다.

다음으로 내가 제출한 수수료가 누구한테 얼마나 가는지 설명하겠다.
수수료 계산 및 정산 과정
1. EntryPoint가 스마트 지갑(사용자)로부터 징수할 수수료 계산
EntryPoint는 UserOp의 Gas 관련 값들로부터 선취할 requiredPrefund를 계산한다.
function _getRequiredPrefund(
MemoryUserOp memory mUserOp
) internal pure returns (uint256 requiredPrefund) {
unchecked {
uint256 requiredGas = mUserOp.verificationGasLimit +
mUserOp.callGasLimit +
mUserOp.paymasterVerificationGasLimit +
mUserOp.paymasterPostOpGasLimit +
mUserOp.preVerificationGas;
requiredPrefund = requiredGas * mUserOp.maxFeePerGas;
}
}
내가 제출한 UserOp의 값을 참고해서 계산하면
requiredGas = verificationGasLimit(358,217) + callGasLimit(17,955) + preVerificationGas(51,811) = 427,983
requiredPrefund = requiredGas * maxFeePerGas = 427,983 * 1584335215(wei) = 0.000678068538321345 ETH
사용자가 EntryPoint에게 전송할 선취 수수료는 0.000678068538321345 ETH이다. 하지만 무조건 requiredPrefund만큼 요청하는 것이 아니다.
2. EntryPoint가 스마트 지갑의 validateUserOp 호출
validateUserOp 인터페이스는 다음과 같다.
interface IAccount {
function validateUserOp
(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
external returns (uint256 validationData);
}
세번째 인자를 보면 missingAccountFunds라는 값이 있다. 이는 위에서 계산된 requiredPrefund에서 현재 스마트 지갑이EntryPoint에 예치한 금액을 뺀 값이다. 즉, 선취해야할 총액은 requiredPrefund이고, missingAccountFunds는 그중에 지금 부족한 양이다. 만약에 EntryPoint에 예치되어 있는 사용자의 잔액이 충분하거나 paymaster가 활성화되어있으면 missingAccountFunds는 0이다.
missingAccountFunds는 아래와 같이 계산한다.
function _validateAccountPrepayment(
uint256 opIndex,
PackedUserOperation calldata op,
UserOpInfo memory opInfo,
uint256 requiredPrefund,
uint256 verificationGasLimit
)
internal
returns (
uint256 validationData
)
{
unchecked {
MemoryUserOp memory mUserOp = opInfo.mUserOp;
address sender = mUserOp.sender;
_createSenderIfNeeded(opIndex, opInfo, op.initCode);
address paymaster = mUserOp.paymaster;
uint256 missingAccountFunds = 0;
if (paymaster == address(0)) {
uint256 bal = balanceOf(sender);
missingAccountFunds = bal > requiredPrefund
? 0
: requiredPrefund - bal;
}
try
IAccount(sender).validateUserOp{
gas: verificationGasLimit
}(op, opInfo.userOpHash, missingAccountFunds)
returns (uint256 _validationData) {
validationData = _validationData;
}
...
지금 살펴보는 트랜잭션의 경우 스마트지갑을 최초로 배포한 상황이기 때문에, EntryPoint에는 사용자가 예치한 이더가 없으므로 missingAccountFunds = requiredPrefund가 된다. 트래잭션을 tenderly로 살펴보면 missingAccountFunds 인자값을 확인할 수 있다.

스마트 지갑에서는 아래 코드를 통해 missingAccountFunds를 caller인 EntryPoint에게 전송한다. 이렇게 징수한 이더는 나중에 Bundler에게 수수료 정산 시 활용된다.
function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
external
payable
override
onlyEntryPoint
returns (ValidationData validationData)
{
...
assembly {
if missingAccountFunds {
pop(call(gas(), caller(), missingAccountFunds, callvalue(), callvalue(), callvalue(), callvalue()))
//ignore failure (its EntryPoint's job to verify, not account.)
}
}
...
}
어셈브리 코드 의미는 다음과 같다.
if (missingAccountFunds != 0) {
(bool ok,) = payable(msg.sender).call{value: missingAccountFunds}("");
ok; // 무시
}
다음으로 수수료 징수와 이더 전송 이후에 일어난 3번 동작에 대해서 알아보겠다.

3번을 보면 EntryPoint가 Bundler에게 이더를 전송하는 과정이다. 왜 전송할까? 결국 전체 트랜잭션의 송신자는 Bundler이므로, 네트워크 수수료는 Bundler가 먼저 지불한다. 하지만 Bundler는 자원봉사자가 아니므로 EntryPoint가 스마트지갑(또는 paymaster) 예치금에서 actualGasCost만큼을 beneficiary(bundler)에게 정산한다. 이 값은 바깥 트랜잭션의 gasUsed와 동일한 측정이 아니라 EntryPoint의 정산 산식이며, preVerificationGas 등이 포함된다.
💩 EntryPoint는 가스비를 안내나? 중간에 EntryPoint가 스마트 지갑의 validatesUserOp()를 호출하지 않나?
=>
EntryPoint는 가스비를 지불하지않는다. EVM에서 가스는 항상 바깥 트랜잭션 서명자가, 여기서는 Bundler(EOA)가 지불한다. validateUserOp는 해당 트랜잭션안의 내부호출일 뿐이다.
EntryPoint는 가스를 지불하지않고, 남은 가스를 받아서 쓴다. 최종 청구서는 tx.origin인 Bundler에게 간다.
Bundler가 먼저 지불한 transaction fee는 0.000352730541947544 ETH이고, EntryPoint가 Bundler에게 전송한 금액(actualGasCost)은 0.00035656662642448 ETH이다. actualGasCost의 값이 transaction fee보다 크다. 어째서 actualGasCost값이 transaction fee보다 커지게 된 것일까?
💩 트랜잭션은 한번 뿐인데 어떻게 서로 다른 가스 비용이 산출되는 걸까?
=>
일(트랜잭션)은 한번인데 계산기가 2개인 것에 비유할 수 있다.
우선 트랜잭션은 1개이므로 실제로 사용된 가스는 323,112이다. transaction fee는 이 가스 값과 Bundler가 제시한 maxPriorityFeePerGas에 의해 다음과 같이 계산된다.

transaction fee = 트랜잭션 gasUsed × 번들러가 트랜잭션에 적은 가스 가격(Base fee + MaxPriorityFee)
= 323,112 × 1.091666487 Gwei
= 0.0003527 ETH
EntryPoint에서 계산하는 actualGasCost는 대략 다음과 같이 계산된다.
actualGasUsed = (검증에서 줄어든 gasleft)
+ (실행에서 줄어든 gasleft)
+ preVerificationGas
actualGasCost = actualGasUsed × UserOp 가스 가격
actualGasUsed = 326,561
UserOp 가격 = min(1.584 Gwei, 1.0895 + 0.00239) = 1.09188 Gwei
actualGasCost = 약 0.0003566 ETH
우선 actualGasUsed 계산에서 UserOp에서 제시한 값인 preVerificationGas를 빼고 생각해보자. EntryPoint가 gas를 측정하는 구간은 검증루프(validateUserOp) 실행 전후 gasleft() 차이와 실행루프(execute) 실행 전후 gasleft() 차이를 측정한다. 즉, 각 루프에서 소비된 가스양을 측정하는 것이다. 여기까지만 봐도 actualGasCost값이 transaction fee보다 커질 이유가 없다. 그런데 앞선 가스 측정은 전체 트랜잭션에서 소비되는 모든 가스 소모를 측정한 것이 아니다.
transaction fee를 다시보자. 여기서 네트워크가 메긴 gasUsed by Txn(323,112)는 EntryPoint의 handleOps 실행가스만 있는 것이 아니다. 트랜잭션 제출 자체로 소모되는 가스 21,000과 calldata(여기서는 handleOps 인코딩)가스도 종합적으로 포함되어 있는 것이다. 즉 EntryPoint의 handleOps가 돌기 전에 프로토콜에 의해 이미 가스를 소모한 상태이다. 그러므로 EntryPoint에서 측정한 가스는 트랜잭션에서 소모한 가스를 모두 반영하지 못한다.
[Gas Used by Txn]
21,000 + calldata 가스 + handleOps 실행 가스 + EntryPoint 고정 오버헤드
이렇듯 가스가 진짜로 쓰였지만, EntryPoint에서 측정할 수 없는 몫을 청구하는 필드가 preVerificationGas인것이다. 이때 UserOp이 alt-mempool에서 거절되거나, bundler가 손해 보는 것을 막기 위해 slack(여유)을 잡는다. 견적시점과 제출시점이 다르기 때문에 번들 위치에 따른 메모리나 비용이 달라질 수 있기 때문이다.
두번째 UserOp 제출(0.001ETH 전송)
앞에서는 첫번째 UserOp 제출이어서 지갑 배포 후 이더를 전송했다. 이때 EntryPoint가 약 0.000678 ETH를 가져갔고 그 중 실제로 절반 정도만 썼다. 그럼 EntryPoint에 예치되어 있는 ETH양도 절반만큼 남았을 것이다. 예측해 볼 수 있는건 두번째로 0.001ETH를 전송할 때는 EntryPoint가 내 스마트 지갑으로부터 수수료를 걷어가는 일이 없을 것이다. 즉 missingAccountFunds값이 0일 것이다. 왜나면 EntryPoint의 내 잔고가 충분하기 때문이다.
실제로 0.001ETH를 보낸 트랜잭션 결과는 다음과 같다.

첫번째 트랜잭션에서 보였던 스마트지갑이 EntryPoint에게 ETH를 전송하는 걸 볼 수 없다. missingAccountFunds값이 0인걸 볼 수 있다.

EntryPoint내의 내 스마트지갑 잔고도 한번 확인해보겠다. 최초에 0.000678068538321345 ETH만큼 EntryPoint에 전송했고 이후 두번의 UserOp으로 소비한 수수료(actualGasCost)는 ( 0.00035656662642448 + 0.000149014833316345 ETH)이므로 어느정도 남은 것을 확인할 수 있다.

계정 추상화라는 개념을 접하고 bundler와 entrypoint의 동작에 대해서 어느 정도 이해하고 있었지만, 수수료라던가 스마트 지갑이 어떻게 배포되는지에 대해서는 정확히 알지 못했다. 그런데 직접 간단하게 구현을 통해 실제 트랜잭션을 확인하니 명확히 이해가 갔다. 다음에는 이번에 생성한 Kernel 스마트 지갑에 입출금 정책을 만들어서 적용시켜보겠다
References
zerodevapp/kernel at release/v3.1
GitHub - zerodevapp/kernel at release/v3.1
https://docs.zerodev.app/. Contribute to zerodevapp/kernel development by creating an account on GitHub.
github.com
ERC-4337: Account Abstraction Using Alt Mempool
ERC-4337: Account Abstraction Using Alt Mempool
Account abstraction without consensus-layer protocol changes, instead relying on higher-layer infrastructure.
eips.ethereum.org
account-abstraction/contracts/core/EntryPoint.sol at releases/v0.7 · eth-infinitism/account-abstraction
Contribute to eth-infinitism/account-abstraction development by creating an account on GitHub.
github.com
'Web3 > Ethereum' 카테고리의 다른 글
| Authorization_list의 튜플 값 검증 및 서명 복원 과정, EIP7702 test in Geth & 테스트넷에서의 동작 확인 (0) | 2026.08.26 |
|---|---|
| EIP-7702: Set Code for EOAs (Add a new tx type that permanently sets the code for an EOA) (0) | 2026.08.24 |
| EIP-1599 | 가스 요금 개선안 (0) | 2025.11.04 |
| EIP vs ERC (0) | 2025.11.04 |
| ERC-20 Token과 ETH 전송의 차이 (0) | 2025.11.01 |
- Total
- Today
- Yesterday
- DEFI
- price manipulation
- ethereum
- puzzlewallet
- 계정추상화
- ethrnaut
- solidity
- eip7702
- Ethernaut
- MagicNumber
- 토큰전송
- dynamic array slot
- smart contract
- type2 transaction
- ERC20
- erc4337
- view functions
- Blockchain
- 이더넛
- oracles
- rebase token
- 블록구조
- storageslot
- Delegatecall
- eip1599
- 모빌리티 보안
- gaslimit
- aderyn
- DexTwo
- 토큰화주식
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |