01 · Orientation
先拆掉三个误会
先建立一个不会在后面反复坍塌的最小模型。
目录原题 · 合约账户:没有私钥,由部署的代码控制
以太坊把世界状态组织成“地址 → 账户状态”的映射。 经典入门模型把账户分为两类:外部账户 EOA 由私钥签名控制; 合约账户由 EVM 字节码的执行规则控制。两者都可以有地址、持有 ETH, 也都可以成为消息调用的目标。1
“没有私钥”就是没有任何人可以影响它。
合约地址没有一把原生私钥,但代码可以把某个 EOA 设为管理员,也可以要求多签、时间锁、DAO 投票,甚至故意不留任何管理员。
合约像后台程序,会按时间表自己醒来做事。
普通合约是被动程序。它必须在某笔顶层交易所引发的执行中被调用;所谓自动化服务,最终也要有人或服务把交易送进链。
“由代码控制”就等于去中心化、不可升级、没有风险。
代码可能写着“owner 一人可升级”,也可能存在漏洞。代码控制只说明状态转换的入口,不替代码的治理质量与安全性背书。
传统合约账户,是一个拥有地址、余额、持久存储和运行时代码的链上状态对象。 它不能签发顶层交易;当消息调用抵达时,EVM 执行它的代码, 代码可以在协议允许的范围内读写状态、转移价值或继续调用其他账户。
学完本课,你应该能做到
读懂账户
解释 nonce、balance、storageRoot 与 codeHash 各自承诺什么。
分清动作
区分顶层交易、消息调用与区块浏览器里的“内部交易”。
追踪身份
沿调用链判断 msg.sender、tx.origin 与真正被读写的 storage。
识别边界
看懂代理升级、合约钱包、SELFDESTRUCT 与 EIP-7702 的现代语义。
02 · Account anatomy
一条地址背后,只有四个协议字段
“合约”不是一团代码;它首先是世界状态中的一条账户记录。
地址是 20 字节的索引。执行客户端用它找到一条账户状态; 账户状态再通过哈希指向代码与存储。把界面、Solidity 源码和项目名字暂时拿走, 协议真正维护的是下面这四个值。1
Interactive · state x-ray
切换三种账户剖面
- nonce
- 0x04 作为该合约创建新合约时使用的序号;不是它“发过几笔交易”。
- balance
- 2.400000000000000000 ETH 这个地址拥有的原生 ETH,以 wei 计数;不包含 ERC-20 与 NFT。
- storageRoot
- 0x7d…91 · non-empty 该地址持久化键值存储的密码学承诺根,状态可跨交易保留。
- codeHash
- keccak256(runtime bytecode) 指向部署完成后保存的 EVM 运行时代码;收到消息调用时执行。
codeHash 不是 Solidity 源码
EVM 执行的是编译后的字节码。链上账户保存 codeHash,
客户端按哈希找到运行时代码;开发者公开的 Solidity 源码通常由区块浏览器做
“源码验证”,证明它能编译出同一份字节码。验证源码很重要,
但它不是账户四字段之一。4
storage、memory 与 transient storage 不是一回事
| 区域 | 活多久 | 属于谁 | 典型用途 |
|---|---|---|---|
| Storage | 跨交易持久存在 | 某个账户地址 | owner、余额映射、订单状态、代理实现地址 |
| Memory | 当前调用帧结束即消失 | 当前 EVM 调用帧 | 临时字节数组、ABI 编解码、返回数据 |
| Transient storage | 同一交易内共享,交易结束清空 | 执行中的账户 | 跨内部调用的短期锁与临时状态 |
经典 EOA 与传统合约账户,逐项对照
| 维度 | 经典 EOA | 传统合约账户 |
|---|---|---|
| 原生控制入口 | 与地址对应的私钥签名 | 消息调用抵达后执行 runtime code |
| 地址来源 | 公钥 Keccak-256 哈希的末 20 字节 | CREATE / CREATE2 规则推导 |
| 能否发起顶层交易 | 能 | 不能;只能在执行中继续消息调用 |
| 收到消息调用 | 经典模型下没有代码可执行 | 加载运行时代码并建立 EVM 调用帧 |
| Nonce 的主用途 | 交易顺序与防重放 | 创建新合约时的地址序号 |
| 创建成本 | 链下生成密钥本身不产生链上费用 | 执行 initcode 与保存 runtime code 消耗 Gas |
| 能否持有 ETH | 能 | 能 |
Solidity 的 private 只是禁止别的合约用语言级接口直接访问。
storage 仍进入公共链状态,观察者可以读取原始存储槽。
03 · Control
控制,不是拥有一把看不见的钥匙
EOA 与合约账户的根本差异,是状态改变前经过哪一道授权门。
EOA 的原生授权门由协议固定:验证签名并恢复 sender。 合约账户的授权门由代码定义:EVM 执行函数,只有所有条件都通过, 那条状态转换路径才会继续。
两类账户的“控制门”
EOA · protocol gate
签名是否来自这条地址?
协议按固定密码学规则验证交易签名、nonce、余额与费用条件。
- 01对交易载荷求哈希
- 02从签名恢复公钥与地址
- 03检查 sender、nonce 与支付能力
- 04有效交易进入状态转换
Contract · programmable gate
代码允许这次动作吗?
协议不认识“owner”。owner、多签和治理权限都是字节码与 storage 定义的应用规则。
- 01消息调用抵达合约
- 02按 calldata 选择代码分支
- 03检查调用者、签名、时间或投票
- 04通过则写状态或继续 CALL;失败则 REVERT
Interactive · 2-of-3 vault
让一个合约钱包决定是否放行
假设金库代码写死了规则:Alice、Bob、Carol 三位 owner 中至少两人对同一动作签名, 并且 24 小时时间锁已经结束,合约才允许向外转账。
因此,“谁控制合约”有三层答案
直接控制者
运行时代码决定哪些调用路径可以改变该地址的 ETH 与 storage。
规则修改者
若代码支持升级或管理员函数,owner、多签或 DAO 可能改变未来行为。
触发者
任何人都可能发起调用;但“能按按钮”与“调用会被允许”是两回事。
不可救援边界
若没有提款路径或所有授权凭证丢失,不存在一把“合约私钥”绕过代码救出资产。
04 · Creation
合约账户不是注册出来的,是执行出来的
部署本身就是一次状态转换:先运行初始化代码,再把返回值保存为运行时代码。
创建合约时,输入不是“请保存这段源码”。交易或另一个合约提供 initcode;EVM 执行它。initcode 可以运行 constructor、 写初始 storage,最后返回一段字节。只有这段返回字节成为新账户的 runtime code。2
从源代码到链上账户的五步
编写与编译
Solidity / Vyper 源码编译成 initcode 与将被返回的 runtime code。
提交创建
EOA 发部署交易,或执行中的合约调用 CREATE / CREATE2。
推导地址
协议根据创建者、nonce,或 salt 与 initcode 哈希计算 20 字节地址。
运行 initcode
constructor 执行并写初始 storage;失败时该创建子执行回滚。
保存 runtime
initcode 返回的字节成为账户代码;部署者为计算与代码存储支付 Gas。
地址如何在账户出现前就被算出来?
last20( keccak256( RLP([creator, creatorNonceBeforeIncrement]) ) )
last20( keccak256( 0xff ++ deployer ++ salt ++ keccak256(initcode) ) )
CREATE 的地址取决于实际创建者及其创建 nonce。若 Alice 调用 Factory,
再由 Factory 创建新合约,公式里的 creator 是 Factory,不是 Alice。
CREATE2 额外承诺 salt 与 initcode 哈希,
因而可以在部署前计算“反事实地址”。6
公式使用创建动作发生前的 creator nonce;主网新创建的合约账户 nonce 通常按 EIP-161 从 1 起步。内部 CREATE / CREATE2 失败时,调用者会得到失败结果并可选择继续; 它不必然让整笔顶层交易一起 REVERT。15
CREATE2 不会覆盖旧代码。若目标地址已有非零 nonce 或非空代码,创建必须失败; 但目标地址只是提前收到了一些 ETH,并不会单独造成地址冲突。 7
合约地址是从部署条件推导的结果,不对应部署者持有的一把密钥。 “我知道这个地址怎样算出来”与“我能为这个地址签交易”毫无等价关系。
05 · Message calls
合约不会自己醒来
一切用户级执行都嵌在某笔顶层交易之中,但一笔交易可以展开成整棵调用树。
普通合约账户不能签名、广播,也不能成为一笔传统顶层交易的 sender。
但当它已经因为消息调用而运行时,可以用 CALL、
STATICCALL、DELEGATECALL 或 CREATE
同步触发更多执行。3
14
Interactive · one transaction
逐步走过一棵调用树
“内部交易”是区块浏览器语言,不是协议里的第二种交易
浏览器常把合约发出的 CALL 显示为 internal transaction,方便人阅读。 但它没有独立签名、独立交易 nonce 或独立交易哈希。更严谨的叫法是 内部消息调用或 call trace。 如果金库后续调用失败且异常没有被处理,storage 写入、价值转移、日志和创建结果 可随执行一起回滚;但顶层 sender nonce 不回滚,已经消耗的 Gas 仍然收费。 对 EIP-7702 的 type-4 交易,执行开始前已处理的有效代码委托也不会因后续执行失败而回滚。 10
钱包用 eth_call 查询 view 函数时,节点会本地模拟执行;
它不进入区块、不产生持久状态,也不是一笔已经上链的交易。
同一个 view 函数若被链上合约内部调用,仍会消耗这笔交易的 Gas。
06 · Execution context
执行谁的代码,改谁的状态?
调用不是只跳到另一个函数;每一层都会建立一份精确的执行上下文。
EVM 每进入一层调用,都要确定:当前地址是谁、加载哪段代码、读写哪份 storage、
直接调用者是谁、带来多少 ETH、能否写状态,以及还剩多少 Gas。
CALL、DELEGATECALL 与 STATICCALL
正是在这些维度上做出不同选择。5
Interactive · execution frame
同样调用实现合约,三种语义
CALL · enter callee context
Proxy 0xProxy
│
└─ CALL 0xLogic
code = Logic.code
storage = Logic.storage
address(this) = 0xLogic
普通 CALL 链里,msg.sender 随直接调用者逐层变化
| 执行位置 | msg.sender | tx.origin | 应该用谁做普通授权 |
|---|---|---|---|
| 合约 A | Alice | Alice | msg.sender |
| 合约 B | 合约 A | Alice | msg.sender |
| 合约 C | 合约 B | Alice | msg.sender |
msg.sender 是当前层的直接调用者;tx.origin
是最外层交易 sender。后者穿过整条调用链不变,因此恶意合约可以诱导用户先调用自己,
再转调一个错误使用 tx.origin 做授权的受害合约。
不要用 tx.origin 判断“是不是本人”或“有没有合约参与”。
这里说的逐层变化针对 CALL / STATICCALL;DELEGATECALL 会保留外层
msg.sender 与 msg.value。
实现代码会读写调用者的 storage。若实现使用的槽位布局与代理不兼容, 或升级权限被夺走,固定地址上的全部状态都可能被破坏。可升级不是代码不变性的反例; 它是固定代理代码主动留下的一条“借用别处代码”的路径。
07 · Asset truth
合约究竟把资产“放”在哪里?
ETH、ERC-20 与 NFT 都可以归属于同一地址,但三者写在不同账本。
“金库合约持有 100 ETH、10,000 USDC 和一枚 NFT”是方便人类理解的说法。
协议实际看到的是三处不同状态:ETH 在金库账户自己的 balance;
USDC 与 NFT 的归属则记录在各自代币合约的 storage 中。
一个地址,三本账
ETH
直接进入 0xVault 账户状态的 balance 字段,以 wei 计数。
= 100 ETH
ERC-20
余额是 USDC 合约 storage 中以持有人地址为键的一条记录。
= 10,000 USDC
ERC-721
所有者是 NFT 合约 storage 中 tokenId 对地址的映射。
= 0xVault
代码可以拒收 ETH 吗?答案不是简单的是或否
普通带 value 的 CALL 抵达合约时,会执行 receive()
或 fallback();代码可以接受,也可以 REVERT。但“账户余额增加”
并不总要经过收款方代码。例如另一个合约执行 SELFDESTRUCT
时可把 ETH 余额转给目标地址,而不会调用目标的收款函数。
常规 CALL + value:进入收款合约执行;是否成功由 payable 入口与代码决定。
某些协议语义可直接增加目标 balance。因此“代码没有收款函数”不能证明余额永远为零。
能拥有,不代表一定能取出
资产从某个账本上归属于合约地址,只说明当前状态;它不会自动生成提款能力。 若 runtime code 没有一条能让条件满足后转出 ETH 或调用 token 合约的路径, 资产就可能永久锁在这个地址。这正是“没有私钥救援通道”的实际含义。
08 · Smart accounts
合约钱包:把“谁能签”变成一段程序
它不是给合约偷偷配一把私钥,而是让合约验证用户提交的证明。
多签钱包、社交恢复钱包与限额钱包通常都是合约账户。 人仍可持有私钥、Passkey 或其他凭证,但这些凭证控制的是 代码是否认可一次操作,不是从合约地址反推出一把私钥。
ERC-4337 中,一次智能账户操作怎样上链
合约也可以“签名”,但含义不同
ERC-1271 定义了 isValidSignature(hash, signature):
应用可询问某个合约“你是否认可这份签名代表你”。合约可以要求一把 owner key、
2-of-3 多签、BLS 聚合签名,甚至根据时间和当前状态改变答案。
它返回的是代码判定结果,不是从合约地址恢复出公钥。8
协议验证
签名方案和 sender 恢复规则由交易类型与协议规定;私钥是最终控制根。
合约回答
调用验证函数,代码决定任意字节是否代表当前账户;规则可以是状态相关的。
它把“有效账户操作必须是一把 secp256k1 私钥签出的固定格式交易” 抽象为“账户能用自己的程序证明这次动作有效”。于是批量操作、费用代付、 会话密钥、Passkey、每日限额与社交恢复都能进入同一账户体验。 9
09 · Lifecycle
代码不变,为什么行为还能升级?
理解“不可变”,必须把地址上的代码与系统最终执行的业务逻辑分开。
一个普通、已部署合约地址上的 runtime code 通常不能被就地改写。 但这份固定代码完全可以写着:“把每次调用转交给 storage 里记录的实现地址。” 于是代理地址与资产状态不变,业务逻辑却能通过替换实现地址而升级。
固定代理,借用可替换实现的代码
Stable address
Proxy · 0xVault
用户始终调用这里;ETH、owner、余额与实现地址都留在代理的账户状态。
借代码
留状态
Replaceable logic
Implementation · V2
提供函数实现,但执行时读写的仍是 Proxy storage;升级通常是改实现地址。
SELFDESTRUCT 今天通常不会删除合约
自 Cancun 升级采用 EIP-6780 后,一个早已存在的合约执行
SELFDESTRUCT(beneficiary),会停止当前执行帧并把全部 ETH
转给 beneficiary,但不会删除账户、代码或 storage。
只有合约在同一笔交易中刚创建又 selfdestruct,才保留旧式删除行为。
两个边界要单独记:既存合约若把自己指定为 beneficiary,余额净变化为零;
同交易新建合约若这样做,余额会被销毁。
12
| 场景 | 余额 | 代码 | Storage | 账户 |
|---|---|---|---|---|
| 合约早已存在 | 全部转给 beneficiary;若指向自己则净变化为零 | 保留 | 保留 | 保留 |
| 同一交易内刚创建 | 转移;若指向自己则销毁 | 交易结束删除 | 交易结束删除 | 删除 |
不要把 SELFDESTRUCT 当作可靠的升级、清理或紧急停止方案。 “先销毁、下笔交易再用 CREATE2 原址部署另一套代码”的旧式 metamorphic 模式,对既存合约已不成立;该操作码也处于弃用方向。
10 · 2026 boundary
EIP-7702 以后,旧二分需要加一条脚注
传统合约账户依然没有私钥;变化的是“EOA 必定没有代码”不再是完整事实。
Pectra 已于 2025 年 5 月 7 日在以太坊主网激活 EIP-7702。 EOA 可以签署授权,在自己的 code 字段写入一个 23 字节的 代码委托标记,让调用执行另一地址的代码,同时保留 EOA 自己的地址、余额与 storage 上下文。10 11
经典模型与现代执行边界
Classic mental model
EOA = key, no code
- 私钥签发顶层交易
- 收到普通调用时没有代码可执行
- 适合作为第一层学习坐标
EIP-7702 delegated EOA
Key + delegation indicator
- Authority 私钥签署授权;外层 type-4 交易可由本人或赞助者提交
- 执行时到 delegate_address 取代码
- 代码在 EOA 自己的地址与 storage 上下文运行
- 委托持久存在,直到被替换或清除
只有一笔交易或消息调用的 destination 指向委托 EOA 时, EVM 才解析委托并执行代码。典型自付费流程可以是该 EOA 向自己发送交易, 再由委托代码调用真正目标;赞助流程也可由 relayer 把授权放进外层 type-4 交易。
哪些旧口诀失效了?
| 旧口诀 | 2026 年更准确的说法 | 安全含义 |
|---|---|---|
| EOA 一定没有代码 | 经典 EOA 无代码;委托 EOA 可带代码指示器并执行委托逻辑。 | 不能再把 code length 当作可靠的人类/合约分类。 |
| 有代码的账户不能发交易 | 任意普通代码仍不能;有效 7702 委托标记是协议明确允许的例外。 | 检查“有无代码”不能替代对调用行为的分析。 |
| tx.origin == msg.sender 说明没有中间合约 | 7702 委托 EOA 可在顶层帧内执行代码并继续调用。 | 不要把这个相等关系当防重入或身份保证。 |
| 撤销委托就完全恢复空白 EOA | 代码标记可以清除,但此前委托逻辑写入的 storage 不会自动清空。 | 更换委托必须考虑存储布局与残留状态。 |
“EOA 与合约账户”仍是理解账户来源与顶层交易权限的基础分类; 但“有没有代码”和“能不能表现出可编程行为”已经不是可靠的二元身份测试。 面向 2026 年开发,更安全的默认假设是:任何地址都可能表现出程序化行为。
EOA 的原私钥仍能替换或清除委托。把 EOA 委托给多签逻辑, 并不会自动消除这把私钥作为最终授权根的地位;委托代码自身的签名、 防重放、初始化与 storage 迁移也都是安全关键。
11 · Recap
把整条因果链压缩成一句话
如果你能从输入一路讲到新状态,就真正理解了“代码控制”。
合约账户没有一把可以跳过这条链的原生私钥。它之所以能“转钱”“创建合约” 或“调用 DeFi”,不是因为它在链外签发了新交易,而是因为当前交易的执行 已经进入它的代码路径,并由 EVM 把这些动作纳入同一次原子状态转换。
十个常见误区,逐个翻面
合约没有私钥,所以没有人能控制它。
错误。代码可以指定 owner、多签、时间锁或治理。没有的是协议原生的合约地址私钥,不是所有社会与治理控制。
合约向外转账,就是合约发起了一笔新交易。
错误。通常是当前交易执行树中的内部 CALL,没有独立签名、交易 nonce 或交易哈希。
合约持有 USDC,所以 USDC 在它的 balance 字段里。
错误。balance 只记原生 ETH;USDC 归属记录在 USDC 合约的 storage。
CREATE2 算得出地址,所以部署者拥有该地址的私钥。
错误。地址由部署条件确定;可预测不产生私钥,也不能覆盖已有非空代码或非零 nonce 的账户。
代理合约可升级,说明链上代码可以被随意改写。
错误。代理自己的代码通常不变;它固定地通过 DELEGATECALL 借用 storage 指向的另一份实现代码。
现代 SELFDESTRUCT 总会删除代码和 storage。
错误。对早已存在的合约,它通常只转移全部 ETH 并停止当前帧,代码、storage 与账户仍保留。
有代码的地址一定是传统合约账户。
错误。EIP-7702 委托 EOA 可带 23 字节代码指示器;预编译等协议边界也让 code length 分类不可靠。
没有代码的地址一定是普通 EOA。
错误。构造函数执行期间新地址尚无 runtime code,预编译有协议行为而不靠普通账户代码,空代码账户也可能存在。
tx.origin 是最初用户,所以最适合做授权。
错误。恶意中间合约与 7702 执行都能破坏这种假设;普通授权应基于当前调用的 msg.sender 与明确签名验证。
代码控制就天然代表去中心化与安全。
错误。代码可能有单一管理员、升级入口或漏洞。应分别审查执行规则、升级治理与实际密钥分布。
六题理解测验
01 · 传统合约账户为什么不能成为顶层交易 sender?
02 · 部署完成后,账户保存的是哪段代码?
03 · Alice → A → B 的 CALL 链中,在 B 内谁是 msg.sender?
04 · DELEGATECALL 最关键的语义是哪一项?
05 · “0xVault 持有 10,000 USDC”在状态里通常怎样表示?
06 · 关于 EIP-7702,哪一句最准确?
12 · Sources
术语表与一手资料
关键事实以 Ethereum 官方文档、执行规范与已定稿 EIP 为依据;页面核对日期为 2026-07-25。
本课最小术语表
| 术语 | 本课中的精确定义 |
|---|---|
| Account | 世界状态中由地址索引的一条 nonce、balance、storageRoot、codeHash 记录。 |
| Contract account | 拥有运行时代码、在消息调用到达时按代码执行的账户;传统合约账户没有原生交易签名私钥。 |
| Transaction | 进入区块的顶层、带签名指令;可产生一棵内部消息调用树。 |
| Message call | EVM 执行中的同步调用,建立新调用帧;不是独立顶层交易。 |
| Runtime code | 部署 initcode 执行后返回并保存在账户上的 EVM 字节码。 |
| Smart contract wallet | 用合约代码验证授权并执行用户意图的账户系统,可实现多签、恢复、限额与费用代付。 |
| Delegated EOA | 通过 EIP-7702 持有代码委托标记、可在自身上下文执行委托逻辑的 EOA。 |
-
01
Ethereum.org · Ethereum accounts 账户类型、四个账户字段、EOA 密钥与“账户不等于钱包”的官方入门说明。
-
02
Ethereum.org · Ethereum Virtual Machine 状态转换、合约创建、消息调用、持久 storage、memory 与 transient storage。
-
03
Ethereum.org · Transactions 顶层交易、合约创建交易、合约调用与 eth_call 的官方说明。
-
04
Ethereum.org · Verifying smart contracts 源码、编译结果与链上字节码验证之间的关系。
-
05
Ethereum.org · Opcodes for the EVM CALL、DELEGATECALL、STATICCALL、CREATE、CREATE2 与 SELFDESTRUCT 的当前操作码语义索引。
-
06
EIP-1014 · Skinny CREATE2 CREATE2 地址公式、initcode 哈希与地址冲突条件。
-
07
EIP-684 · Revert creation in case of collision 目标已有非零 nonce 或非空代码时,合约创建必须失败。
-
08
ERC-1271 · Standard Signature Validation Method for Contracts 合约通过 isValidSignature 自定义“什么签名代表我”。
-
09
ERC-4337 · Account Abstraction Using Alt Mempool UserOperation、Bundler、EntryPoint、智能账户验证与 Paymaster 的规范。
-
10
EIP-7702 · Set Code for EOAs set-code 交易、委托指示器、执行解析、EOA 发起交易例外与安全边界。
-
11
Ethereum Foundation · Pectra Mainnet Announcement Pectra 主网激活时间与 EIP-7702 账户能力概览。
-
12
EIP-6780 · SELFDESTRUCT only in same transaction 现代 SELFDESTRUCT 对余额、代码、storage 与账户删除的分场景规则。
-
13
Ethereum Execution Specifications · Prague fork types 当前执行层账户数据类型、序列化字段与代码委托时代的规范表示。
-
14
EIP-3607 · Reject transactions from senders with deployed code 普通非空代码账户不得成为交易 sender;EIP-7702 后,有效委托指示器是协议规定的例外。
-
15
EIP-161 · State trie clearing 合约创建前 nonce 的递增规则,以及状态回滚时空账户删除也必须回滚的边界。
本课先回答“合约账户由什么控制”。下一阶段应把调用成本接上: 第 6 章追踪一笔交易,第 7 章拆 Gas,第 8 章深入 Stack、Memory、 Storage 与 Opcode,第 9 章再进入智能合约完整生命周期。