第二十章 · 第二课 / APPLICATION SECURITY

以太坊没有算错,钱为什么还是会

每个节点都可以一模一样地执行一段有漏洞的逻辑。 共识保证大家得到同一个结果,却不保证这个结果符合应用原本的意图。

  • 约 55 分钟
  • 12 个章节
  • 10 组原生图示
  • 4 个互动模型
01 / CONSENSUS ≠ INTENT

一致,不等于正确

共识回答“大家是否接受同一状态”;应用安全回答“这个状态是否满足业务不变量”。两者不能互相替代。

02 / TRUST IS A GRAPH

信任不是一个开关

代码、管理员、价格数据、实现合约与外部依赖共同组成信任图。最弱的一条边可能决定全部资金的安全。

03 / SECURITY IS A PROCESS

审计不是终点

规格、不变量、测试、审计、时间锁、监控与应急响应是一条连续防线;任何单点都不是“绝对安全证明”。

本课目录 · 12 sections
  1. 先划清安全边界
  2. 四个入口,一张风险图
  3. 重入:控制权回来了
  4. 权限:谁能改变什么
  5. 预言机:正确执行错误事实
  6. 升级:不可变代码如何变化
  7. 组合性:安全会沿依赖传播
  8. 纵深防御:把失败当作输入
  9. 现场审查:十二问
  10. 一页收束
  11. 六题自检
  12. 术语与一手资料
01 / 12

Orientation

先划清边界:安全不是一个总开关

目录 PDF 对本课的正式定义是“应用层风险:重入攻击、权限配置、预言机与升级风险”。四类问题看似不同,实质都发生在协议规则之上的应用信任边界。

假设一笔攻击交易有合法签名、支付了足够 Gas,被验证者收入区块,EVM 逐条执行 opcode, 最终所有节点算出同一个状态根。以太坊有没有“出错”?没有。若合约允许攻击者在余额尚未清零时重复提款, 网络只是忠实执行了它收到的程序。

这正是应用层安全最反直觉的地方:确定性不会判断意图。协议可以保证交易排序、执行规则与状态转换 对所有节点一致,却不知道开发者本来想让“每人只能提款一次”,也不知道某个价格源刚刚被操纵。智能合约控制真实资产, 交互通常不可逆,部署后的代码又默认难以修改,所以应用需要比普通后端更明确的安全规格。 [1]

两种“正确”:协议正确与应用正确回答不同问题

FIG 01
左边出问题是协议或共识风险;右边出问题是本课的应用风险。应用运行在安全的底座上,不等于应用自动继承了完整安全性。

Safety property

不该发生的事永远不发生

例如总负债不能超过允许上限、没有 MINTER_ROLE 的账户不能铸币、同一债权不能被支付两次。

Liveness property

该发生的事最终能发生

例如诚实用户最终能提款、价格源故障后系统可以恢复、治理不会因为所有密钥丢失而永久锁死。

Invariant

每次状态转换后都必须成立

不变量是可检查的系统承诺。它比“函数看起来没问题”更接近真正的安全规格。

Threat model

明确谁、以什么能力、攻击什么

攻击者可借到巨额瞬时流动性、部署恶意合约、组合任意公开调用,并观察待处理交易。

02 / 12

One model, four entrances

四个入口,一张风险图

重入利用控制流,权限利用控制权,预言机利用输入事实,升级利用代码可变性。把入口分开,才能为每类风险设计对应防线。

应用安全的四个控制面

FIG 02
四类风险会互相放大:升级管理员被攻破可以植入重入逻辑;错误权限可以替换预言机;错误价格又能触发大规模清算。
application safety ≈ invariants × control-flow discipline × least privilege × data integrity × change governance 这是思考框架,不是可计算概率。使用乘号强调:任何一项接近零,其他项再强也可能无法保住系统。

攻击者不必“破解以太坊”。公开网络给了所有人相同接口:部署合约、发起嵌套调用、借用原子流动性、 读取状态、组合多个协议。安全设计必须假定调用者会选择最不利的输入与顺序,而不是按产品界面的正常路径操作。 Solidity 官方安全说明因此把每次外部合约交互都视为潜在危险,并建议从控制流、调用失败、授权与资源边界一起考虑。 [2]

Reentrancy failure

旧状态被重复消费

系统把外部调用当成普通函数返回,却忘了对方可以在返回前重新进入自己或关联合约。

Authority failure

权限大于职责

一个账户拥有不必要的能力,或者权限转移、撤销与延迟没有形成完整流程。

Information failure

错误事实被当成真值

合约逻辑完全正确,却对被操纵、陈旧、单位错误或暂时不可用的数据作出正确反应。

Change failure

修复通道变成攻击通道

代理允许补丁和迭代,也让升级授权、初始化和存储布局成为新的关键安全面。

03 / 12

Reentrancy

你把控制权交出去,它可以在“回来之前”再进一次

重入不是两笔交易同时执行,而是同一笔交易的调用栈中,外部合约在原函数完成前回调。危险来自“可观察的中间状态”。

EVM 调用是同步的。合约 A 执行 call、调用代币钩子或另一个未知合约时,会暂停自己的当前函数, 把剩余 Gas 与控制权交给 B。B 可以返回,也可以先调用 A 的另一个入口。只要整笔交易尚未结束,嵌套调用都共享同一份可变化的链上状态。

外部调用的真实含义:不是“发送后继续”,而是“暂停并交出控制权”

FIG 03
任何外部合约调用都可能重入,不只发送 ETH。标准代币回调、NFT 安全接收钩子、任意插件或未知依赖都需要同样的威胁模型。
Vulnerable orderINTERACT → EFFECT
function withdraw() external {
  uint256 amount = balances[msg.sender];
  require(amount > 0);

  // 先交出控制权
  (bool ok,) = msg.sender.call{value: amount}("");
  require(ok);

  // 回来以后才清零:太晚
  balances[msg.sender] = 0;
}
Safer orderCHECK → EFFECT → INTERACT
function withdraw() external {
  uint256 amount = balances[msg.sender];
  require(amount > 0);

  // 先提交本地状态
  balances[msg.sender] = 0;

  // 最后才交出控制权
  (bool ok,) = msg.sender.call{value: amount}("");
  require(ok);
}

右侧采用 Checks-Effects-Interactions:先检查前置条件,再写入本地效果,最后与外部系统交互。 当攻击者回调时,余额已经是 0,第二次提款会失败。Solidity 文档明确提醒,重入并不只来自 ETH 转账, 任何对其他合约的调用都可能交出控制权,多合约场景还会让依赖状态在背后发生变化。 [2]

Interactive lab 01

一笔交易,怎样长出第二层提款?

逐步推进调用栈。模型故意保留漏洞顺序,用来观察“旧余额”如何在嵌套调用中被重复消费。

1 / 5
账面余额 10 ETH
Vault 真实资产 40 ETH
已向攻击者支付 0 ETH
Alice 的提款交易进入 Vault.withdraw()。合约读取攻击者账面余额 10 ETH,状态尚未改变。

重入不止一种形状

Single-function

同一函数重入

withdraw() 在完成前再次调用 withdraw(),是最经典也最容易画出的形态。

Cross-function

跨函数重入

外部调用期间进入另一个共享状态函数,例如一边提款,一边通过 transfer() 消耗同一余额。

Cross-contract

跨合约重入

多个模块共享会计或价格假设;A 正在更新时,回调经 B 读取或改变了尚未一致的系统状态。

Read-only

只读重入

回调没有重复写入原合约,却读取到暂时失真的汇率或余额,其他协议再依据这个视图作出有价值的决定。

四道防线,各自解决不同问题

  1. 先写不变量例如“任意时刻,已支付总额不得超过已扣减债权”。不变量决定测试真正要攻击什么。
  2. 调整顺序使用 Checks-Effects-Interactions,在交出控制权前提交本地状态;跨函数共享状态也要统一审视。
  3. 增加互斥锁ReentrancyGuard 可阻止受保护入口在活动调用期间再次进入,但不能修复错误会计或未保护的旁路。
  4. 隔离外部交互优先采用 pull payment,让接收者主动领取;限制回调面,避免在一个函数中混合多项敏感状态变化。

OpenZeppelin 的工具库提供 ReentrancyGuardPausable 等构件; 它们是经过维护的基础模块,不会自动替你选择正确的函数边界、角色或不变量。 [3]

04 / 12

Access control

真正的问题不是“有没有管理员”,而是“谁能改变什么、何时生效”

访问控制把主体映射到能力。配置错误既可能让陌生人进入,也可能让合法管理员拥有超出职责的爆炸半径。

Solidity 的 publicexternal 描述函数怎样被调用,不描述谁被授权。 private 只限制其他合约从源码接口直接调用,不会把链上数据变成秘密。真正的授权必须在每个敏感入口明确检查调用者、角色与条件。

权限拓扑:主体、角色、能力与保护措施必须逐边核对

FIG 04
“去中心化”不能替代逐函数权限图。应当能回答:每条边由谁授予、谁能撤销、是否有延迟、密钥失陷后最多损失什么。

Ownable、角色与时间锁不是三个等级,而是三种工具

Ownable

一个主体,简单清楚

适合单一管理员的简单系统。若 owner 能完成所有高危操作,它也成为最集中的失败点。

Role-based access

按职责拆分能力

铸币者、暂停者、预言机管理员、升级管理员可以分离,贯彻最小权限原则。

Multisig

把单密钥变成阈值决策

降低单点失陷概率,但阈值签名完成后仍可能立即执行错误或恶意操作。

Timelock

把执行与决定拉开

为公开监控、取消与用户退出提供时间;代价是紧急修复也会变慢,因此常与窄权限暂停角色配合。

Interactive lab 02

同一组功能,配置怎样改变爆炸半径?

切换四种教学配置。风险分数仅表示相对趋势,不是任何真实系统的安全评级。

单一热钱包管理员

一个在线私钥同时拥有铸币、暂停、改价与升级权限。配置最简单,但单点失陷即可控制全部关键面。

92/ 100 · RELATIVE RISK
密钥单点风险极高
权限爆炸半径全部
公开反应窗口几乎无
事件隔离能力困难
提示:多签主要改变“攻破控制权的门槛”,角色拆分改变“一次失陷能做多少事”,时间锁改变“恶意决定到执行之间有多少反应时间”。

OpenZeppelin 将“每个组件只能做其工作所需之事”概括为最小权限原则,并特别提醒 DEFAULT_ADMIN_ROLE 默认既管理其他角色又管理自己,因此风险很高。时间锁让维护操作先排队, 用户有机会审查并退出,但角色不可用也可能让系统永久卡住。 [4]

权限审查要看完整生命周期

  1. 授予初始角色在构造或初始化时给了谁?是否可能被陌生人抢先初始化?
  2. 使用每个敏感函数是否真的使用正确 modifier?内部旁路、批处理器与代理入口是否绕过检查?
  3. 转移所有权是否采用两步接受?新旧管理员交接期间,谁可以操作?错误地址能否撤回?
  4. 撤销角色更换后旧主体是否彻底移除?链上事件和监控能否确认当前持有人?
  5. 失效密钥被盗、签名者离线或治理受攻击时,是否有取消、暂停、恢复和退出路径?
05 / 12

Oracles

合约算得完全正确,也可能依据了一个错误事实

EVM 不能直接请求网页或交易所 API。预言机把外部事实变成链上状态;从那一刻起,数据质量就进入应用的安全边界。

区块链节点必须对同一输入算出同一结果。如果合约在执行中各自访问互联网,不同节点可能收到不同价格、超时或响应, 共识就无法确定。因此,外部数据必须先由某个机制观察、验证、聚合,再通过交易写到链上供合约读取。

价格进入借贷协议前,至少经过六道边界

FIG 05
“使用了知名预言机”只描述第 3–4 层的一部分。市场选择、调用方检查、资产换算与故障处理仍由具体应用负责。

Ethereum.org 将预言机问题拆成正确性、可用性和激励相容性:数据是否来自正确来源且未被篡改,是否及时可获得, 报告者是否承担说谎成本。多节点、多来源与经济激励能降低单点风险,但不会让任何数据天然成为“绝对真相”。 [5]

01 / SOURCE来源集中多个节点若都读取同一家交易所,仍共享同一个信息故障点。
02 / LIQUIDITY市场可操纵薄流动性池的现货价可被一笔大交易短时推离真实市场。
03 / FRESHNESS数据陈旧最后一个价格曾经正确,不代表在当前区块仍适合借款或清算。
04 / UNITS精度与报价方向8 位与 18 位小数、ETH/USD 与 USD/ETH 混淆都会造成数量级错误。
05 / AVAILABILITY更新中断网络、报告者或 L2 排序器故障时,应用必须选择暂停、降级或替代源。
06 / BOUNDARY极端市场脱锚、停牌、市场关闭与快速波动可能超出平时参数假设。

闪电贷通常是放大器,不是根因

闪电流动性让攻击者无需长期资本,就能在一笔原子交易中借入大量资产、推动薄池现货价、让受害协议按扭曲价格放贷, 最后偿还借款。若受害协议读取的是可被单笔交易操纵的价格,根因是价格源与风险窗口不匹配; 闪电贷只是把达到操纵规模的门槛压低。

Interactive lab 03

同一笔价格尖峰,四种读取策略会怎样放大或拒绝它?

教学算例:10 ETH 抵押物,正常价 $2,000,最高借款率 75%。忽略手续费、滑点与清算细节。

单池现货价normal ≈ $2,000
协议接受价格$2,800
抵押物10 ETH
最高借款率75%
允许借出$21,000
判断接受被瞬时推高的价格
协议在攻击交易内读取同一薄流动性池的现货价。闪电流动性放大了临时买单,错误估值与借款发生在同一原子交易中。

安全读取不是一个函数名,而是一组前置条件

accept(price) only if price > 0 AND now - updatedAt ≤ maxAge AND decimals are normalized AND deviation is within policy AND dependency health is acceptable 不同 feed、网络与资产的接口和更新机制不同。实际代码必须按目标数据源的当前文档、市场时段、heartbeat、偏差阈值和 L2 故障模型设计,不能机械复制这段教学条件。
  1. 先定义价格含义报价资产、基准资产、小数位、单位、符号与有效范围必须写入规格和测试。
  2. 核对新鲜度读取更新时间并按业务风险设置 maxAge;陈旧时应安全失败,而不是悄悄沿用。
  3. 降低可操纵性使用与风险规模匹配的深度、多源聚合或时间窗口;评估操纵成本而非只看品牌。
  4. 设计故障模式区分暂停新增风险、允许还款、允许提款、启用备用源;“全部继续”与“全部冻结”通常都过于粗糙。
  5. 限制连锁后果借款上限、单资产限额、价格偏差阈值与清算节流可把单次错误限制在可承受范围。
06 / 12

Upgradeability

代码默认不可变,但“调用哪段代码”可以被设计成可变

代理模式把地址与状态留在 Proxy,把业务逻辑放在 Implementation。升级不是改写旧字节码,而是让代理以后 delegatecall 到另一个实现。

普通合约部署后,其地址上的运行时代码不会像服务器文件那样被覆盖。可升级系统通过间接层实现变化: 用户始终调用代理地址;代理读取实现地址,再用 delegatecall 执行实现代码。 代码来自实现,msg.sender、余额与存储上下文却属于代理。

Proxy 模型:地址与状态不动,逻辑指针发生变化

FIG 06
EIP-1967 为实现、Beacon 与管理员地址规定标准存储槽,便于工具识别和监控代理,同时避开编译器常规分配的应用存储。

EIP-1967 解决的是“代理元数据放在哪里、怎样被工具发现”的标准化问题,不替应用保证谁有权升级或新逻辑是否安全。 标准建议实现或管理员槽变化时发出事件,因为持续监控这些变化本身就是安全要求。 [8]

Interactive lab 04

逻辑换了,旧状态为什么会突然“变成别的东西”?

代理的槽位不会跟着源码变量名移动。切换升级方式,观察 V2 怎样解释同一组旧字节。

Proxy storage interpreted by V2

Outcome

安全扩展:只在末尾新增变量

V1 的 owner、balances、totalDebt 保持原槽位;V2 把 reserveFactor 追加到 slot 3。旧状态仍按原含义读取。

真实升级应使用与具体框架相符的存储布局校验、部署模拟与升级前后不变量测试;“看起来只改了几行”不足以证明兼容。

可升级系统新增了五类核心风险

  1. 升级授权失守错误的 _authorizeUpgrade、过宽角色或管理员密钥失陷,可以把全部逻辑换成恶意实现。
  2. 初始化被抢实现没有构造器上下文;代理若未原子调用 initializer,后来者可能先认领 owner 或关键参数。
  3. 存储布局冲突重排、删除、改变变量类型或继承顺序,会让新代码用新名字解释旧槽位,造成静默数据破坏。
  4. 升级能力被锁死UUPS 实现若丢失兼容接口或授权路径,系统可能无法继续升级;错误回滚假设也会制造永久故障。
  5. 治理绕过承诺即使代理技术完全正确,单一管理员的即时升级仍可以突然改变费用、资产控制和退出规则。

OpenZeppelin 的代理说明将状态与逻辑分离,并强调升级前后必须保持存储布局兼容; 可升级合约还需要用只执行一次的 initializer 代替普通构造器逻辑。 [6] [7] UUPS 通过实现合约中的升级逻辑和授权决定谁能更换实现,EIP-1822 则定义了可兼容实现的接口思路。 [9]

Technical control

布局与初始化校验

使用成熟工具检测升级兼容;部署即初始化;锁定实现合约的初始化入口;为每次迁移写状态断言。

Authorization control

多签、角色与明确授权

升级权独立于日常参数权;UUPS 的授权函数必须明确受控;定期核对实际持有人。

Temporal control

时间锁与公开监控

实现地址、代码哈希与升级 calldata 在执行前可见;高风险变化留出评审与退出窗口。

Structural control

标准槽与命名空间

EIP-1967 隔离代理元数据;ERC-7201 规范命名空间存储布局,帮助模块化系统减少槽位冲突。

ERC-7201 规定了命名空间存储布局的注释与槽位推导方式,让多个模块可以各自拥有不与常规存储树碰撞的区域。 它提升可分析性,但编译器不会仅凭注释替你强制正确实现,开发者仍需负责布局一致。 [10]

07 / 12

Composability

单个合约看起来安全,整个系统仍可能从依赖处破裂

可组合性让协议像积木一样复用流动性、价格、代币和治理,也让一个错误沿着调用、资产与经济依赖传播。

现代 DApp 很少只是一份合约。借贷市场可能依赖多个代币、价格源、DEX、利率模型、清算机器人、 代理管理员和前端路由。每个组件都有自己的代码与治理边界,系统安全因此是一张图,而不是一份审计报告。

依赖图:Lending Pool 的状态由四个外部控制面共同塑造

FIG 07
中心合约代码没有变化,任一依赖的回调、价格、流动性或治理发生变化,仍可能破坏原先成立的不变量。

把“事故原因”拆成根因、放大器与后果

Root cause

真正违反不变量的缺口

例如读取可操纵现货价、外部调用前未扣余额、升级授权未保护。修复必须落到这里。

Amplifier

让缺口迅速变大的条件

闪电流动性、高借款上限、集中权限、深度组合与缺少速率限制会扩大一次失误。

Propagation

风险跨依赖传播

错误价格触发清算,清算砸穿流动性,价格进一步下跌,另一个抵押协议继续连锁清算。

Impact

最终损失不止被盗金额

坏账、资产冻结、治理停摆、错误铸币、偿付顺序不公和信任损失应分别衡量。

system exposure = direct privileges + callable dependencies + economic dependencies + upgrade paths “没有直接调用某合约”不代表没有依赖:抵押物价格、DEX 流动性、跨链资产兑付或治理投票权都可能形成经济依赖。

一个虚构借贷事故,四类风险如何串在一起

08 / 12

Defense in depth

把失败当作设计输入,而不是发布后的意外

安全不是在上线前“找完所有 bug”,而是从规格到响应持续减少漏洞概率、缩小爆炸半径并提升发现与恢复速度。

五层防线:每一层都假定上一层可能漏掉问题

FIG 08
纵深防御不追求“每一层都完美”,而是避免同一错误一路无阻地穿透所有层。

先把自然语言承诺变成可测试不变量

偿付能力系统可归属资产始终足以覆盖可赎回债权,或明确记录坏账。
守恒除明确定义的铸造与销毁外,用户余额变化之和与资产流向一致。
权限封闭无对应角色的任意地址无法执行敏感状态转换。
升级保持升级后旧账户、余额、债务与角色仍按升级前语义解释。
价格边界陈旧、非正、单位错误或超出允许偏差的数据不能新增系统风险。
可恢复性单个操作员离线或某依赖故障时,关键偿还与退出路径仍有明确策略。

测试工具各自看见不同种类的问题

Unit & integration

已知路径是否按预期

验证边界值、错误分支、依赖回调与升级迁移;测试应包含恶意合约,不只正常用户。

Fuzz & invariant

随机序列能否破坏承诺

让工具生成输入与调用顺序,持续检查总量、偿付、权限和会计不变量。

Static analysis

代码结构暴露了什么模式

寻找未检查调用、危险授权、重入面、影子变量与已知模式;快速但会有误报和漏报。

Formal verification

模型是否证明给定性质

把目标性质与程序模型交给求解器;证明只覆盖写进规格的性质与工具实际建模的边界。

Independent audit

外部专家挑战假设

架构、业务逻辑与实现共同评审;范围、版本和已知限制必须公开。

Bug bounty & monitoring

上线后继续发现与响应

为披露提供激励;监控角色、实现、价格、资金流与异常事件,并准备可演练的响应手册。

Solidity 的 SMTChecker 可对断言、状态变量与外部调用模型进行形式化分析;它将外部调用视为未知代码, 正体现了安全设计不应假定被调用方一定温顺。 [12] 源码验证则帮助使用者确认公开源码与链上字节码对应,但“代码已验证”只证明对应关系,不证明逻辑安全。 [11]

四类风险的最小防线矩阵

  1. 重入CEI + 统一互斥边界 + pull 模式 + 跨函数/跨合约不变量测试 + 外部回调最小化。
  2. 权限最小权限 + 角色拆分 + 多签 + 两步转移 + 高危操作时间锁 + 角色与事件持续监控。
  3. 预言机多源/抗操纵设计 + 新鲜度和单位检查 + 偏差限制 + 资产上限 + 明确的故障降级策略。
  4. 升级成熟代理标准 + 原子初始化 + 布局校验 + 升级模拟 + 明确授权 + 延迟与实现地址监控。
09 / 12

Field guide

面对任何 DApp,用十二问找到真正的控制点

普通使用者不必读完全部 Solidity 才能提出好问题;开发者也不应从代码细节开始,而应先画资产、权限、数据、变化与失败路径。

  1. 资产在哪里?由哪个合约持有,用户拿到的是原生资产、份额、债权还是跨链映射?谁能移动或冻结它?
  2. 最重要的不变量是什么?总资产、总负债、份额价格、抵押率与权限边界怎样定义?有没有机器可检查的断言?
  3. 外部调用在哪里?发送 ETH、调用代币、钩子、DEX、预言机或插件之前,本地状态是否已经稳定?
  4. 哪些函数是高危入口?提款、铸币、参数、暂停、救援、迁移与升级是否都使用了正确访问控制?
  5. 当前权限由谁持有?是单一 EOA、多签、DAO、时间锁还是 AccessManager?阈值、成员与角色能否链上核对?
  6. 权限变化多久生效?升级与关键参数是否有公开排队期?紧急路径能否绕过延迟,绕过范围多大?
  7. 价格从哪里来?有多少独立来源,市场深度如何,读取的是现货还是时间/节点聚合,单位与方向是什么?
  8. 数据失效时怎样处理?更新时间、异常范围、网络停机、市场关闭与脱锚分别触发暂停、备用源还是沿用旧值?
  9. 合约能升级吗?代理类型、当前实现、管理员、时间锁与初始化状态是什么?升级历史和事件是否公开?
  10. 依赖也能变化吗?代币、预言机、DEX、桥和治理自身是否可升级或暂停?上游变化会破坏哪些本地假设?
  11. 发生异常时谁先发现?监控哪些角色、实现槽、价格偏差、资金流与失败事件?告警是否有人值守并有权限行动?
  12. 最坏损失被限制了吗?单资产上限、速率限制、分仓、保险或应急退出能否把一次失误限定在系统可承受范围?

证据强度从低到高

Misread 01

“不可变,所以一定更安全”

不可变减少升级攻击面,也让已部署漏洞难以修复。关键是系统承诺、复杂度与恢复需求怎样取舍。

Misread 02

“多签,所以已经去中心化”

还要看阈值、成员独立性、权限范围、签名流程、时间锁与能否单独替换成员。

Misread 03

“价格在链上,所以不可操纵”

上链使记录公开且确定,不保证生成价格的市场、聚合或更新时间可靠。

Misread 04

“加了 nonReentrant 就没有重入”

保护范围可能不完整,跨合约只读状态仍可能失真;互斥锁也不会修复错误业务不变量。

Misread 05

“审计发现 0 个严重问题”

只代表给定版本和范围内没有被报告;不代表未来升级、经济环境和依赖始终安全。

Misread 06

“暂停开关能处理一切”

若告警太慢、权限被盗、暂停覆盖错误或连偿还也冻结,开关本身可能成为新的风险。

10 / 12

Recap

把整课压成四个“谁”

遇到复杂协议时,先不背漏洞名字。问清控制权、授权者、事实来源和代码变化者,安全边界就会显形。

security claim → explicit invariant → adversarial test → limited authority → monitored operation → rehearsed response 先把“安全”改写成可验证承诺,再用攻击者视角检验;上线后持续核对权限、数据、实现与资产流。
  1. 重入的核心外部调用交出控制权;危险不是“回调”本身,而是回调看到并消费了尚未稳定的状态。
  2. 权限的核心授权必须逐函数、逐角色、逐生命周期设计;主体被攻破时的最大能力比“管理员人数”更重要。
  3. 预言机的核心链上确定性只能确定地使用输入;数据来源、时间、单位、操纵成本和失效策略都属于应用逻辑。
  4. 升级的核心代理让状态地址保持不变、逻辑可替换;修复能力同时创造授权、初始化、布局与治理风险。
  5. 系统的核心安全沿依赖图传播。任何上游可变性、回调或经济假设都应进入威胁模型和故障预算。
11 / 12

Self check

六题,检验你是否真的找到应用层边界

每题选择一个最准确的答案。关闭 JavaScript 或打印页面时,完整解析仍会直接显示。

01一笔重入攻击被最终确认,最准确的描述是什么?

答案:协议正确执行了错误的应用规则。共识保证一致,不替应用判断业务意图。

02为什么 Checks-Effects-Interactions 能阻断经典提款重入?

答案:交出控制权前先提交关键状态。回调读取到的是已清零或已扣减后的状态。

032 / 3 多签最直接改变了哪项安全属性?

答案:攻破管理权的门槛。角色拆分缩小权限范围,时间锁才主要增加反应窗口。

04为什么“价格已经写在链上”仍不足以证明它安全?

答案:错误或陈旧数据也能被确定地记录。数据生成与调用方校验都属于信任边界。

05代理升级时,在 V2 变量列表最前面插入新变量的主要风险是什么?

答案:旧状态被按新的存储布局错读。布局兼容是代理升级的核心约束之一。

06哪项最能体现纵深防御?

答案:不变量、测试、限权、监控与响应共同工作。每一层都假定其他层可能漏掉问题。

12A / 12

Glossary

核心术语

先记每个词解决的具体问题,再去读实现细节。

Reentrancy
一次外部调用返回前,被调用方又进入调用方或关联组件,使尚未稳定的状态被重复读取或消费。
CEI
Checks-Effects-Interactions:检查条件、提交本地状态、最后外部交互的控制流顺序。
Reentrancy guard
在受保护调用活动期间拒绝再次进入的互斥机制;保护边界与业务不变量仍需正确设计。
Least privilege
每个主体只拥有完成职责所必需的最小能力、范围和持续时间。
RBAC
Role-Based Access Control:先定义角色与能力,再把角色授予账户或合约。
Timelock
让高危操作从排队到执行至少等待一段时间,为审查、取消或退出提供窗口。
Oracle
把链外或难以直接获得的信息采集、验证并写入链上,供智能合约确定性使用的系统。
TWAP
Time-Weighted Average Price:在一段窗口内按时间聚合价格,用持续操纵成本换取对短时尖峰的抵抗力。
Proxy
保留稳定地址与状态、把调用委托给可替换实现合约的间接层。
delegatecall
在调用方的地址、余额、调用上下文和存储中执行另一合约代码的 EVM 调用方式。
Initializer
代理环境中替代构造器完成状态初始化的函数,必须防止重复或被陌生人抢先调用。
Storage layout
状态变量与 EVM 存储槽之间的映射;可升级实现必须兼容旧布局。
Invariant
系统在允许的每次状态转换前后都应成立的可检查性质。
Blast radius
一个密钥、组件或假设失效后,最多能影响的资产、功能、用户与时间范围。
12B / 12

Primary sources

继续深入的一手资料

以下均为 Ethereum、Solidity、OpenZeppelin 或 EIP 官方原文。课程正文做了教学抽象,生产实现应回到目标版本的最新文档与代码。

[01] ETHEREUM.ORG

Smart contract security

安全开发总览:访问控制、测试、审计、恢复、重入与预言机操纵。

[03] OPENZEPPELIN

Contracts Utils

ReentrancyGuard、Pausable 与低层调用等维护中的合约构件。

[04] OPENZEPPELIN

Access Control

Ownable、角色、最小权限、AccessManager 与时间锁设计。

[05] ETHEREUM.ORG

Oracles

链外数据为何需要预言机,以及正确性、可用性与激励问题。

[06] OPENZEPPELIN

Proxy Upgrade Pattern

delegatecall、代理状态、实现逻辑、存储冲突与构造器限制。

[08] ERC-1967

Proxy Storage Slots

实现、Beacon 与管理员的标准槽位,以及升级事件与监控建议。

Chapter 20 · Lesson 02 complete

真正的安全,不是相信不会出错,而是让每一种错误都更难发生、更早暴露、损失更小。