目录
-
- [1. 以太坊只有两种账户](#1. 以太坊只有两种账户)
- [2. EOA:私钥就是一切](#2. EOA:私钥就是一切)
- [3. 合约账户:代码即法律](#3. 合约账户:代码即法律)
- [4. 交易的"源头"永远是 EOA](#4. 交易的"源头"永远是 EOA)
- [5. 怎么在链上判断一个地址是 EOA 还是合约](#5. 怎么在链上判断一个地址是 EOA 还是合约)
-
- [5.1 标准方法:查 `code`](#5.1 标准方法:查
code) - [5.2 这个判断的两个经典陷阱](#5.2 这个判断的两个经典陷阱)
- [5.1 标准方法:查 `code`](#5.1 标准方法:查
- [6. EIP-7702:EOA 和合约的边界被打破](#6. EIP-7702:EOA 和合约的边界被打破)
-
- [6.1 它解决什么问题](#6.1 它解决什么问题)
- [6.2 它怎么工作](#6.2 它怎么工作)
- [6.3 结果:出现了"既是 EOA 又有 code"的混合体](#6.3 结果:出现了"既是 EOA 又有 code"的混合体)
- [7. 账户抽象 (Account Abstraction) 与抽象钱包](#7. 账户抽象 (Account Abstraction) 与抽象钱包)
-
- [7.1 什么是账户抽象](#7.1 什么是账户抽象)
- [7.2 抽象钱包能做什么](#7.2 抽象钱包能做什么)
- [7.3 两条主流实现路线对比](#7.3 两条主流实现路线对比)
- [8. 安全平台视角:为什么必须重视这个区分](#8. 安全平台视角:为什么必须重视这个区分)
-
- [8.1 风险画像依赖账户类型](#8.1 风险画像依赖账户类型)
- [8.2 EIP-7702 带来的新检测点](#8.2 EIP-7702 带来的新检测点)
- [8.3 检测要点小结](#8.3 检测要点小结)
- [9. 转账到 EOA vs 转账到合约的风险对照](#9. 转账到 EOA vs 转账到合约的风险对照)
-
- [9.1 转账到 EOA 的问题](#9.1 转账到 EOA 的问题)
- [9.2 转账到合约的问题](#9.2 转账到合约的问题)
- [9.3 对照总结](#9.3 对照总结)
- [10. 一句话总结](#10. 一句话总结)
面向新人和安全研究者。读完应该能回答:以太坊有哪两种账户?EOA 和合约账户到底差在哪?怎么在链上判断一个地址是哪种?EIP-7702 出现后两者的边界为什么变模糊了?这对安全检测意味着什么?
1. 以太坊只有两种账户
以太坊上所有的"地址",本质上只分两类:
| EOA (Externally Owned Account) | 合约账户 (Contract Account) | |
|---|---|---|
| 中文 | 外部拥有账户 / 普通账户 | 合约账户 |
| 由谁控制 | 私钥(一对公私钥) | 代码(部署时写死的逻辑) |
| 有没有代码 | 没有(code 为空) |
有(code 非空) |
| 能否主动发起交易 | 能(签名后广播) | 不能(只能被调用后执行) |
| 创建方式 | 生成私钥即拥有,免费、无需上链 | 必须发一笔部署交易、花 gas |
| 典型例子 | MetaMask 钱包、交易所提币地址 | USDT 合约、Uniswap、多签钱包 |
两者地址格式完全一样 (都是 20 字节、0x 开头 42 字符),从地址字符串本身看不出是哪种------必须查链上状态。
2. EOA:私钥就是一切
EOA 是绝大多数用户手里的"钱包地址"。
- 私钥 → 公钥 → 地址:随机生成一个 256 位私钥,经椭圆曲线(secp256k1)算出公钥,再 keccak256 取末 20 字节得到地址。
- 生成是离线的、免费的:你在本地生成一个私钥就"拥有"了对应地址,链上完全不知道它存在,直到它第一次收款或发交易。
- 只有私钥能动它:任何花费、转账、合约调用,都必须用私钥对交易签名。私钥丢了 = 资产永久锁死;私钥泄露 = 资产被盗。
- EOA 不能"自己"做任何事:它不会定时执行、不会响应事件,必须由持有私钥的人(或程序)主动发起并签名一笔交易。
一句话:EOA = "一把钥匙 + 一个余额"。没有逻辑,只有签名权。
3. 合约账户:代码即法律
合约账户是部署在链上的程序。
- 由部署交易创建 :开发者发一笔
to为空的交易,把编译好的字节码(bytecode)写进链上,得到一个合约地址。 - 地址是确定性算出来的 :
- 普通部署:
address = keccak256(rlp(deployer, nonce))[12:] CREATE2部署:address = keccak256(0xff ++ deployer ++ salt ++ keccak256(bytecode))[12:]------ 可以在部署前就预知地址(很多攻击和"地址挖矿"靠它)。
- 普通部署:
- 没有私钥 :合约账户不存在私钥,不能主动发起交易,只能在被某笔交易(来自 EOA 或另一个合约)调用时,按代码逻辑执行。
- 有持久化存储:合约可以读写自己的 storage(状态变量),EOA 没有这个概念。
- 代码通常不可变 :部署后字节码固定(除非用代理模式 Proxy +
delegatecall做可升级),这也是"代码即法律"的由来。
一句话:合约账户 = "一段代码 + 一块存储 + 一个余额",被动执行,没有签名权。
4. 交易的"源头"永远是 EOA
这是一个关键且容易被忽略的事实:
链上每一笔交易(transaction)的发起方
tx.origin一定是某个 EOA。
合约之间可以互相调用(这叫 internal call / message call),但整条调用链的最初触发者必须是一个 EOA 签名的交易。合约自己不会凭空开始执行。
EOA (签名发起 tx)
└─> 合约 A.foo()
└─> 合约 B.bar() ← 这些都是 internal call
└─> 合约 C.baz()
tx.origin= 最初那个 EOA(整条链路不变)msg.sender= 当前这一跳的调用者(每一跳都变)
很多合约的权限判断、防重入、防钓鱼逻辑都依赖区分这两者。(注意:用 tx.origin 做鉴权是著名的反模式,易被钓鱼合约绕过。)
5. 怎么在链上判断一个地址是 EOA 还是合约
5.1 标准方法:查 code
最权威的方法是看这个地址有没有部署代码:
javascript
// JSON-RPC
eth_getCode(address, "latest")
// 返回 "0x" → 没有代码 → EOA
// 返回 "0x6080..." → 有代码 → 合约账户
solidity
// 合约内(老写法)
function isContract(address a) internal view returns (bool) {
return a.code.length > 0; // 等价于 extcodesize(a) > 0
}
5.2 这个判断的两个经典陷阱
- 合约构造期间
code.length == 0:合约在自己的 constructor 执行期间,代码还没写入,此时对它extcodesize返回 0。攻击者可以在 constructor 里调用你的"仅限 EOA"函数来绕过isContract检查。 - EIP-7702 之后,EOA 也可能有 code (见下一节)。所以"有 code 就是合约"这个判断在 2025 年之后不再绝对成立。
更稳妥的现代判断:结合
eth_getCode的内容(7702 的 code 是特殊的0xef0100 ++ 地址前缀)+ 是否有过签名交易 nonce 等多信号。
6. EIP-7702:EOA 和合约的边界被打破
2025 年以太坊 Pectra 升级引入了 EIP-7702,这是理解"EOA contract"这个说法的关键。
6.1 它解决什么问题
传统 EOA 太"笨":不能批量交易、不能代付 gas、不能设置消费限额、丢私钥就全完。智能合约钱包(账户抽象 AA)能做这些,但用户得迁移到一个新合约地址,麻烦。
EIP-7702 让你现有的 EOA 地址临时"挂载"一段合约代码,从而在保留原地址和私钥的前提下,获得合约钱包的能力。
6.2 它怎么工作
-
用户用 EOA 私钥签一个特殊的 authorization(授权),指定一个"实现合约"地址。
-
这笔授权上链后,该 EOA 的
code字段会被设成一个特殊指针:0xef0100 ++ <实现合约地址> // delegation designator
-
之后任何对这个 EOA 的调用,都会像 delegatecall 一样跑那个实现合约的逻辑,但用的是 EOA 自己的存储和余额。
-
用户随时可以撤销或更换委托。
6.3 结果:出现了"既是 EOA 又有 code"的混合体
| 属性 | 升级后的 7702 EOA |
|---|---|
| 还有私钥吗? | 有,私钥仍可直接签名发交易 |
eth_getCode 返回? |
非空 (0xef0100... 前缀) |
| 能像合约一样被调用执行逻辑吗? | 能 |
| 还是不是 EOA? | 是------tx.origin 仍可是它,仍由私钥控制 |
这就是俗称的 "EOA contract" / "智能 EOA":一个账户同时具备了私钥控制权(EOA 特性)和可编程逻辑(合约特性)。
7. 账户抽象 (Account Abstraction) 与抽象钱包
EIP-7702 只是更大图景里的一块。它背后的整体方向叫账户抽象 (Account Abstraction, AA) ,做出来的钱包就叫智能合约钱包 / 抽象钱包 (Smart Account)。
7.1 什么是账户抽象
一句话:
让"账户"不再被私钥死死绑定,而是用代码自定义"怎样才算一笔合法交易"。
传统 EOA 的所有硬伤都源于"账户 = 一把私钥":私钥丢了资产永久没了、必须自己持有 ETH 付 gas、一次只能发一笔交易、签名规则写死(只能 secp256k1 单签)、私钥泄露就全盘被盗。
AA 的思路是把"验证一笔交易是否有效"的逻辑,从协议写死的"验证私钥签名",换成一段可编程的合约代码------你想怎么验就怎么验。
7.2 抽象钱包能做什么
因为验证逻辑可编程,智能合约钱包能实现 EOA 做不到的事:
| 能力 | 说明 |
|---|---|
| 社交恢复 | 私钥丢了,靠几个"守护人"或其它设备恢复账户,不再一丢全无 |
| Gas 代付 (Paymaster) | 别人/项目方替你付 gas,甚至用 USDT 付 gas,新手无需先买 ETH |
| 批量交易 | 一次签名完成 approve + swap + 转账等多步操作 |
| 多签 / 阈值签名 | 2/3 签名才放行,企业金库常用 |
| 消费限额 / 白名单 | 单笔限额、只能转给白名单地址、超额需额外验证 |
| 会话密钥 (Session Key) | 给游戏/DApp 一个临时受限密钥,免去每步弹窗签名 |
| 自定义签名算法 | 用 passkey / 指纹 / 人脸 (secp256r1) 等,不依赖助记词 |
7.3 两条主流实现路线对比
为了避免混淆,把目前让"账户变智能"的两条主流路线放一起:
| ERC-4337 (智能合约钱包) | EIP-7702 (EOA 挂载代码) | |
|---|---|---|
| 地址类型 | 全新的合约地址 | 沿用原有 EOA 地址 |
| 有无私钥 | 通常无(由合约逻辑控制) | 有(私钥仍然有效) |
| 是否改动协议层 | 否,纯合约 + 独立 mempool (UserOperation) | 是,协议层新增交易类型 |
| 用户迁移成本 | 高(要换地址、转资产) | 低(原地址原私钥直接升级) |
| 典型能力 | 批量交易、社交恢复、gas 代付、会话密钥 | 同上,但保留 EOA 身份 |
ERC-4337 不改协议,靠一套链下基础设施跑起来:
- UserOperation:用户的"意图",不是普通交易,丢进一个独立的内存池
- Bundler:打包者,把多个 UserOperation 捆成真实交易上链
- EntryPoint:统一入口合约,负责校验和执行
- Paymaster:gas 代付合约
EIP-7702 则是协议层改动,让现有 EOA 原地升级(机制见第 6 节)。
两者都是"账户抽象"的实现,可以配合使用(7702 EOA 也能接入 4337 基础设施)。EIP-7702 让海量存量 EOA 用户能低成本享受 AA 能力,被认为是 AA 普及的关键一步。
8. 安全平台视角:为什么必须重视这个区分
账户类型是几乎所有链上安全分析的基础元数据,类型判断错了,后续结论全错。
8.1 风险画像依赖账户类型
- 转账目标是合约还是 EOA :给一个不认识的合约转原生币,可能触发其
receive/fallback逻辑(潜在风险);给 EOA 转账则只是单纯转移。 - 授权对象是合约还是 EOA :
approve给 EOA 几乎无意义(EOA 不能主动花你的额度),approve给合约才是授权钓鱼的关键面。 - 合约的代码可读性:合约可以拉字节码 / 验证源码做静态分析;EOA 没有代码可分析,只能看行为。
8.2 EIP-7702 带来的新检测点
7702 普及后,安全平台需要升级判断逻辑:
对一个地址:
code = eth_getCode(addr)
IF code == "0x":
→ 纯 EOA
ELIF code 以 0xef0100 开头:
→ 7702 委托型 EOA("EOA contract")
提取委托的实现合约地址,分析其逻辑是否恶意
⚠️ 同一 EOA 的委托目标可被更换 → 需持续监控变更
ELSE:
→ 传统合约账户
新增风险面:
- 恶意委托钓鱼:诱导用户对 EOA 签一个 7702 授权,把账户委托给攻击者合约,之后攻击者可凭该逻辑批量转走资产(一次签名 = 交出整个账户的可编程控制权)。
- 委托目标可变:今天委托给良性合约,明天换成恶意合约,地址不变但行为突变,必须监控 delegation 变更事件。
isContract老检测被绕过/误判:依赖"有 code = 合约"做风控的系统需要适配 7702。
8.3 检测要点小结
- 用
eth_getCode而非地址本身判断类型,并识别0xef0100委托前缀。 - 对 7702 EOA,追踪其委托目标合约并分析逻辑、监控目标变更。
- 授权类风险(approve / permit / 7702 authorization)重点看对象是不是可编程合约。
- 注意合约 constructor 期间
code.length == 0的历史绕过手法。
9. 转账到 EOA vs 转账到合约的风险对照
账户类型直接决定一笔转账的风险性质:转 EOA 的风险在"转错对象",转合约的风险在"转了之后出事或拿不回"。
9.1 转账到 EOA 的问题
EOA 收款只是改余额、不触发任何代码,风险都集中在"有没有转对人":
- 地址投毒 / 转错地址 :EOA 是地址投毒的主要受害场景,复制错首尾相同的靓号地址即把钱转给攻击者,链上不可逆、无法找回。
- 死地址 :转给没人持有私钥的 EOA(打错的随机地址、烧毁地址
0x000...dEaD)等于销毁。 - 目标后续可能变 7702 委托账户:今天纯 EOA、明天挂恶意合约逻辑,地址不变但性质变了。
EOA 这边几乎没有"转账动作本身触发风险"的问题,纯粹是"对象对不对"。
9.2 转账到合约的问题
合约收款可能触发代码,且很多合约根本没设计接收某类资产,风险面大得多:
- 永久锁死(最高发) :ERC20 的
transfer只改balances[contract]、不检查对方能否处理。目标合约若没有"取出这种代币"的函数 → 币永远卡死。典型如误转到代币自己的合约地址(USDT 转进 USDT 合约)。 - 原生币触发
receive/fallback:合约没写 payable 接收函数 → 交易 revert ;写了 → 可能重入攻击 ;.transfer()/.send()只给 2300 gas → 对方逻辑稍复杂就 out of gas 失败。 - ERC20 转账不通知接收合约:普通 ERC20 转账不会调用对方回调,合约"不知道收到了币",无从自动处理------这正是锁死的根因。
- 恶意合约:honeypot 故意 revert/扣留、假代币合约、已自毁或失效代理合约,转入即丢。
- 转错入口:把交易所充值(通常是 EOA)误填成合约地址,或转到 DApp 里非存款入口的合约。
9.3 对照总结
| 维度 | 转账到 EOA | 转账到合约 |
|---|---|---|
| 收款时是否执行代码 | 否,只改余额 | 原生币触发 receive/fallback;ERC20 不触发但可能无人能取 |
| 主要风险 | 转错对象(地址投毒、死地址) | 锁死 / revert / 重入 / 被恶意逻辑处理 |
| 钱能否找回 | 转出即不可逆 | 锁死大多不可逆;revert 则没转出去 |
| 典型事故 | 复制错投毒地址、打错地址 | 误转到代币合约本身、转给不支持该资产的合约 |
| 转前该做的检查 | 校验完整地址、用地址簿/ENS、小额先试 | 先 eth_getCode 确认是不是合约;是合约则确认它确为接收该资产而设计、有取出路径、源码已验证/在白名单 |
10. 一句话总结
以太坊只有两种账户:EOA (私钥控制、无代码、能主动发起交易)和合约账户 (代码控制、有存储、只能被动执行)。每笔交易的源头 (
tx.origin) 一定是 EOA;判断类型用eth_getCode而非看地址。EIP-7702 让 EOA 能临时挂载合约代码,诞生了"既有私钥又有 code"的 "EOA contract"(智能 EOA)------它模糊了两者边界,也带来了恶意委托钓鱼等新风险。安全平台必须把"账户类型 + 是否 7702 委托 + 委托目标"作为基础画像持续监控。