实操实验室·安全卷

本卷共 15 个安全实验(实验 39–53),全部聚焦"识别、防御、修复、响应"四件事。

默认技术栈:本地 EVM 链(Anvil / Hardhat 本地节点)、Solidity 0.8.x、Foundry、viem / ethers.js、Node.js 18+,少量 Python / HTML。


全卷安全红线(每个实验都适用,请务必先读)

本卷所有实验只运行于以下四种环境之一:

  1. 本地链(anvil / hardhat node,chainId 31337 等),代币与账户均无真实价值;
  2. 故意设计的教学合约——由你自己部署在本地链上,源码首行必须标注 // 仅本地教学,请勿部署到主网
  3. 浏览器本地模拟——localhost 上的静态页面,不连接任何真实站点、不发起任何真实请求;
  4. 固定历史数据 / 只读公开链上数据——只查询,绝不发起写交易。

本卷绝不提供:

漏洞类实验的产出只有两样东西:如何识别,如何修复。

负责任披露:若你在真实项目中发现漏洞,正确做法是通过项目官方安全渠道或漏洞赏金平台私下报告, 在修复并公告前不公开、不利用。利用漏洞获取资产在绝大多数司法辖区都是犯罪行为。

本卷内容不构成投资建议,也不构成法律建议。


实验39:异常交易识别

等级:进阶 适合角色:安全研究者 / 风控 / 运营 风险等级:无风险(本地链造数据 + 只读公开数据) 预计时间:90 分钟 前置知识:开发卷实验 22、33

安全边界:本实验在本地链上自造交易数据集用于训练检测规则,对真实链只做只读查询,不发起任何交易。

学习目标:建立一套可执行的异常交易检测规则,能从大量正常交易中筛出值得人工复核的少数,并理解"告警不等于攻击"。

场景:你负责监控一个协议的链上活动。每天几万笔交易,你需要一个自动化的第一道筛子。

环境要求:anvil + Node.js + viem。

初始化命令

mkdir -p ~/web3-labs/sec/lab39 && cd ~/web3-labs/sec/lab39 && npm init -y && npm i viem
anvil    # 另一个终端

操作步骤 / 每步预期结果

  1. 在本地链造一个混合数据集:990 笔正常小额转账 + 10 笔带异常特征的交易(超大金额、全新地址首笔即大额、单区块高频、调用少见函数、gas 异常高)。
  2. 实现规则引擎,每条规则输出分数:
const rules = [
  { name: '金额超过历史P99', score: 3, test: t => t.value > p99 },
  { name: '发送方为新地址', score: 2, test: t => t.nonce === 0n },
  { name: '同区块高频调用', score: 2, test: t => sameBlockCount[t.blockNumber] > 5 },
  { name: 'gasPrice 显著高于均值', score: 1, test: t => t.gasPrice > avgGas * 3n },
  { name: '调用敏感函数', score: 3, test: t => SENSITIVE.has(t.selector) },
];
// 总分 >= 5 进入人工复核队列
  1. 跑全量 → 输出告警列表,检查 10 笔异常是否都被命中(召回率)。
  2. 统计误报:多少正常交易被误标(精确率)→ 调整阈值,体会"漏报 vs 误报"的权衡。
  3. 加时序维度:某地址一小时内流出量超过其历史日均 10 倍 → 捕捉"缓慢抽干"型异常。
  4. 建立分级响应:低分记录、中分人工看、高分立即通知负责人并考虑暂停。
  5. 只读验证:用公开 RPC 查询一个公开的协议合约地址的最近区块交易,跑一遍你的规则,观察真实数据下的误报率。

验证命令

node detect.mjs | grep -c "ALERT"     # 期望接近 10(造的异常笔数)
node detect.mjs --stats               # 输出精确率/召回率

完成标准:产出一份规则表,含每条规则的分值、命中率、误报率,以及分级响应流程。

常见错误与排查:阈值写死导致换个协议就全失效 → 用历史分位数动态计算;把合约的正常批量操作当异常 → 需维护白名单。

安全提示:异常检测只是线索生成器。任何告警在人工确认前都不应作为指控依据,更不应据此公开点名(呼应开发卷实验 33)。

重置方法:删除目录;重启 anvil。

进阶挑战:加入"资金来源图谱"维度——若某地址的资金来自已知混币器输出,提高分数。

思考题:攻击者知道你的规则后会怎么规避?规则公开与保密各有什么代价?

对应章节:第 26 章 链上监控与风控


实验40:授权风险分析

等级:进阶 适合角色:所有人(安全关键) 风险等级:无风险(本地链 + 只读查询) 预计时间:80 分钟 前置知识:基础卷实验 14

安全边界:所有授权与撤销操作均在本地链上你自己部署的教学 ERC-20/ERC-721 上进行。对真实地址只做只读的 allowance 查询,不发起任何交易。

学习目标:建立一套完整的授权风险评估方法,能对任意地址产出"授权风险清单",并制定撤销优先级。

场景:一个用了三年的地址,累计给上百个合约授过权。其中哪些是定时炸弹?

环境要求:anvil + Node.js + viem。

初始化命令

mkdir -p ~/web3-labs/sec/lab40 && cd ~/web3-labs/sec/lab40 && npm init -y && npm i viem
anvil

操作步骤 / 每步预期结果

  1. 本地部署 3 个教学代币 + 1 个教学 NFT,向 8 个不同"spender"地址授出不同类型的权限:有限额度、无限额度、setApprovalForAll
  2. 写扫描器:遍历 Approval / ApprovalForAll 事件历史,重建"当前有效授权表"。
// 注意:必须用事件历史 + 当前 allowance 双重确认,
// 因为 allowance 可能已被 transferFrom 消耗或被后续 approve 覆盖
const current = await client.readContract({ address: token, abi, functionName: 'allowance', args: [owner, spender] });
  1. 输出授权表:代币 / spender / 额度 / 是否无限 / 首次授权时间 / spender 是否为合约 / 合约是否开源。
  2. 评分模型:无限额度 +3;setApprovalForAll +4;spender 为 EOA(而非合约)+4(正常协议几乎不会让 EOA 持有授权);授权超过 180 天未使用 +2;spender 合约不可验证 +3。
  3. 排序输出"撤销优先级 TOP 10"。
  4. 在本地链执行撤销(approve(spender,0) / setApprovalForAll(op,false))→ 重新扫描,风险分归零。
  5. 成本估算:算出撤销 N 个授权需要的 gas 总量 → 制定"高危立即撤、中危批量撤"的策略。

验证命令

node scan.mjs <你的本地测试地址>    # 期望输出 8 条授权,撤销后为 0
cast call <token> "allowance(address,address)(uint256)" <owner> <spender> --rpc-url http://127.0.0.1:8545

完成标准:能对任一地址产出带优先级的授权风险清单,并说清每一项的最坏后果。

常见错误与排查:只查事件不查当前值导致误报 → 必须双重确认;漏掉 NFT 的 ApprovalForAll → 它比 ERC-20 无限授权更危险(整个集合)。

安全提示:撤销授权需要付 gas,这是必要的安全成本。已跑路或已被公告存在漏洞的项目,其授权应立即撤销。 使用授权管理工具时,务必从官方渠道进入,工具本身也需要你授权连接钱包——先确认域名。

重置方法:重启 anvil。

进阶挑战:写一个定期任务,每月自动扫描你的地址并把新增授权推送给你。

思考题:如果 ERC-20 标准从一开始就限制授权必须带过期时间,会损失什么便利?现在补救(如 Permit2 式方案)的阻力在哪?

对应章节:第 10 章 交互安全


实验41:本地重入漏洞识别与修复

等级:高级 适合角色:开发者 / 安全研究者 风险等级:中(仅限本地链教学合约预计时间:100 分钟 前置知识:开发卷实验 18、22

安全边界(务必先读):本实验只在本地 anvil 链上、对你自己写的、故意设计成脆弱版本的教学合约进行。 教学合约源码首行必须标注 // 仅本地教学,请勿部署到主网。 本实验的重点是识别模式与修复方法不针对任何真实协议,不提供可用于攻击真实合约的代码或路径。 请勿将本实验的任何合约部署到测试网或主网。

学习目标:亲手看到"外部调用先于状态更新"如何导致余额被重复提取,并掌握三种标准修复:检查-生效-交互(CEI)、重入锁、pull 模式。

场景:一个提款函数先转账、后清零余额。看起来只差一行顺序。

环境要求:Foundry + anvil。

初始化命令

mkdir -p ~/web3-labs/sec/lab41 && cd ~/web3-labs/sec/lab41 && forge init --no-git . && anvil

操作步骤 / 每步预期结果

  1. 写脆弱版教学合约:
// 仅本地教学,请勿部署到主网
contract VulnerableVault {
    mapping(address => uint256) public balances;
    function deposit() external payable { balances[msg.sender] += msg.value; }
    function withdraw() external {
        uint256 bal = balances[msg.sender];
        require(bal > 0, "no balance");
        (bool ok, ) = msg.sender.call{value: bal}("");   // ← 外部调用在前
        require(ok);
        balances[msg.sender] = 0;                        // ← 状态更新在后:这就是漏洞
    }
}
  1. 用 Foundry 测试(forge test)而非手写攻击脚本来演示缺陷存在:写一个测试合约,其 receive() 在收到 ETH 时再次调用 withdraw,断言最终余额异常 → 测试通过即证明缺陷成立。这是标准的安全测试写法,产物是测试用例,不是攻击工具。
  2. 观察结果:单次存入 1 ETH 的账户提走了多于 1 ETH → 缺陷确认。
  3. 修复一(CEI 顺序):把 balances[msg.sender] = 0; 移到 call 之前 → 重跑测试,断言失败(即缺陷消失)。
  4. 修复二(重入锁):加 nonReentrant 修饰符(OpenZeppelin ReentrancyGuard)→ 第二次进入直接 revert。
  5. 修复三(pull 模式)withdraw 只记账,用户另发一笔 claim 领取 → 从架构上消除重入面。
  6. 扩展识别练习:找出跨函数重入(withdrawtransfer 共享状态)、只读重入(view 函数在重入期间返回过期状态)两种变体,并说明为什么单纯的 nonReentrant 不一定够。
  7. 产出识别清单:任何 call / transfer / ERC-777 hook / ERC-721 safeTransferFrom 回调之后是否还有状态写入?

验证命令

forge test -vv
# 脆弱版:重入测试 PASS(说明缺陷存在)
# 修复版:重入测试 FAIL/revert(说明缺陷已消除)

完成标准:能在代码评审中 30 秒内定位违反 CEI 的函数,并给出至少两种修复。

常见错误与排查:以为 transfer(2300 gas)就安全 → gas 成本会变,不可依赖;加了 nonReentrant 但跨合约共享状态仍可重入 → 需在状态层面而非函数层面思考。

安全提示:重入是被讲了十年仍在发生的漏洞,因为它藏在"外部调用"里,而现代合约到处是外部调用(代币回调、预言机、路由)。默认假设:任何外部调用都可能回调你。

重置方法:重启 anvil;forge clean删除教学合约,不要保留到任何部署脚本中。

进阶挑战:写一个静态检查脚本,扫描 .sol 文件中"外部调用之后仍有状态赋值"的函数并告警。

思考题:如果 Solidity 语言层面默认禁止"外部调用后写状态",会损失哪些合法用例?

对应章节:第 27 章 智能合约常见漏洞


实验42:本地访问控制错误

等级:高级 适合角色:开发者 / 安全研究者 / 审计 风险等级:中(仅限本地链教学合约预计时间:90 分钟 前置知识:开发卷实验 23、37

安全边界:本实验只在本地链上对自己部署的教学合约(首行标注 // 仅本地教学,请勿部署到主网)进行。 目的是学会识别缺失的权限校验并补全不涉及任何真实合约,不提供绕过真实权限的方法

学习目标:识别五类典型的访问控制缺陷,并用测试用例证明修复有效。

场景:一个合约有 20 个函数,19 个都有 onlyOwner。第 20 个是初始化函数。

环境要求:Foundry + anvil。

初始化命令

mkdir -p ~/web3-labs/sec/lab42 && cd ~/web3-labs/sec/lab42 && forge init --no-git . && anvil

操作步骤 / 每步预期结果

  1. 部署一个教学合约,故意包含五类缺陷:
// 仅本地教学,请勿部署到主网
contract BadAccessControl {
    address public owner;
    bool private initialized;

    function initialize(address _owner) external { owner = _owner; }        // 缺陷1:可重复初始化
    function setFee(uint256 f) external { fee = f; }                        // 缺陷2:完全缺失修饰符
    function adminWithdraw() external { require(tx.origin == owner); }      // 缺陷3:用 tx.origin 鉴权
    function _internalUpgrade() public { impl = msg.sender; }               // 缺陷4:本应 internal 却是 public
    function emergencyStop() external onlyOwner { paused = true; }          // 缺陷5:单签 EOA 即可冻结全网
}
  1. forge test 用例,逐个断言"非 owner 账户能够改变本不该改变的状态" → 测试通过即缺陷确认。
  2. 逐条修复: - 缺陷1 → 加 initializer 修饰符或 require(!initialized) 并置位; - 缺陷2 → 加 onlyOwner; - 缺陷3 → 改用 msg.sendertx.origin 会把任何被诱导调用的用户当成授权者); - 缺陷4 → 改为 internal; - 缺陷5 → admin 改为多签 + timelock(呼应开发卷实验 37)。
  3. 重跑测试 → 全部断言失败(缺陷消失)。
  4. 建立审查清单:所有 external/public 函数逐个回答"谁可以调?调了会怎样?最坏损失多少?"
  5. forge inspect <合约> methods 列出全部对外方法 → 对照清单确认无遗漏。

验证命令

forge test -vv                                     # 修复前后断言结果应相反
forge inspect BadAccessControl methods | wc -l     # 确认审查覆盖了全部对外方法

完成标准:能对任意合约产出"函数 × 调用者 × 影响"矩阵,且无遗漏的对外函数。

常见错误与排查:以为 private/internal 变量外部读不到 → 链上存储永远可读(cast storage),可见性只限制调用不限制读取;初始化函数忘了在代理部署脚本中原子调用 → 存在被抢先初始化的窗口。

安全提示tx.origin 鉴权是历史上多起钓鱼事故的根源——用户只要调用了攻击者的合约,tx.origin 仍是用户自己。任何情况下都用 msg.sender

重置方法:重启 anvil,删除教学合约。

进阶挑战:用 forge coverage 确认你的权限测试覆盖了每个 external 函数的"非授权调用"分支。

思考题:"默认拒绝"与"默认允许"在合约设计中如何体现?Solidity 的默认可见性设计经历过怎样的演变?

对应章节:第 27 章 智能合约常见漏洞


实验43:Oracle 风险

等级:高级 适合角色:开发者 / 风控 / 协议设计者 风险等级:中(仅限本地链教学合约预计时间:100 分钟 前置知识:开发卷实验 28

安全边界:本实验是开发卷实验 28 的安全深化版,同样只在本地链 + 教学合约上进行(首行标注 // 仅本地教学,请勿部署到主网)。 不提供任何针对真实预言机或真实协议的操纵方法。 产出是风险评估表与防御设计。

学习目标:把预言机风险拆成五个可评估维度(来源、时效、聚合、偏离、依赖),并为一个协议设计完整的防御方案。

场景:你要为一个新协议选预言机方案,需要回答"最坏情况下价格能错多少,错了会损失多少"。

环境要求:Foundry + anvil;开发卷实验 24/26 的 AMM 与借贷合约。

初始化命令

mkdir -p ~/web3-labs/sec/lab43 && cd ~/web3-labs/sec/lab43 && forge init --no-git . && anvil

操作步骤 / 每步预期结果

  1. 搭建对照实验:四个预言机实现(均为本地教学合约)——单一现货池、TWAP、单一外部推送、多源中位数 + 偏离熔断。
  2. 对每个实现施加四种压力(在本地链上模拟): - 瞬时冲击:单笔大额兑换改变池价; - 持续推动:连续多个区块推动价格; - 数据陈旧:价格源停更 6 小时; - 单源失效:某个源返回 0 或异常值。
  3. 记录矩阵:4 实现 × 4 压力 = 16 格,每格记录"价格偏离幅度"与"下游借贷合约是否产生坏账"。
  4. 关键发现(预期): - 现货池对瞬时冲击完全无防御; - TWAP 抵抗瞬时冲击但对持续推动仍有暴露,且引入了滞后(暴跌时清算不及时); - 单一外部源在停更时静默返回旧值——必须加 staleness check; - 多源中位数 + 熔断在四种压力下都能拒绝服务而非给出错误价格。
  5. 实现完整防御模板:
// 仅本地教学,请勿部署到主网
function getPriceSafe() external view returns (uint256) {
    (uint256 p, uint256 updatedAt) = primary.latest();
    require(block.timestamp - updatedAt < MAX_STALENESS, "stale");   // 时效
    uint256 q = secondary.latest();
    require(_deviation(p, q) < MAX_DEV, "deviation");                 // 偏离熔断
    require(p > MIN_PRICE && p < MAX_PRICE, "bounds");                // 合理区间
    return p;
}
  1. 产出风险评估表:来源类型 / 更新频率 / 心跳阈值 / 聚合节点数 / 偏离容忍度 / 失效时行为(返回旧值?revert?暂停?)。
  2. 设计准入规则:哪些资产不允许作为抵押品(低流动性、单一池、无独立价格源)。

验证命令

forge test --match-test testOracle -vv
# 期望:安全版在四种压力下均 revert 或返回稳定值,脆弱版在瞬时冲击下价格剧变

完成标准:产出 4×4 风险矩阵和一份可直接用于协议设计的预言机需求文档。

常见错误与排查:只做偏离检查不做时效检查 → 两个都停更的源可能"一致地错";熔断后 revert 导致清算无法进行 → 需设计降级模式(暂停新借但允许还款)。

安全提示预言机失效时"拒绝服务"几乎总是优于"给出错误价格"。 设计时应明确失效行为,并在文档中告知用户。

重置方法:重启 anvil,删除教学合约。

进阶挑战:加入"价格变化速率限制"(每区块最多变动 x%),观察它如何在剧烈波动中保护协议,又如何在真实暴跌中造成坏账。

思考题:预言机的安全性上限是它所依赖的市场的深度。在深度不足的市场上,还有可能有安全的预言机吗?

对应章节:第 20 章 预言机


实验44:跨链桥验证失效

等级:高级 适合角色:开发者 / 安全研究者 风险等级:中(仅限本地双链教学合约预计时间:100 分钟 前置知识:开发卷实验 31

安全边界:本实验在两条本地 anvil 链上、对自建的教学桥合约(首行标注 // 仅本地教学,请勿部署到主网)进行。 目的是理解"消息验证应该校验什么",不针对任何真实桥,不提供任何攻击真实桥的路径或代码

学习目标:把跨链消息验证拆成六项必检条件,用测试用例证明每项缺失都会导致资金失衡,再逐项补全。

场景:桥的本质是"链 B 相信链 A 上发生了某件事"。这个"相信"由什么保证?

环境要求:两个 anvil + Foundry + Node.js relayer。

初始化命令

anvil --port 8545 --chain-id 1337     # 链 A
anvil --port 8546 --chain-id 1338     # 链 B
mkdir -p ~/web3-labs/sec/lab44 && cd ~/web3-labs/sec/lab44 && forge init --no-git .

操作步骤 / 每步预期结果

  1. 部署开发卷实验 31 的教学桥(Vault + WrappedToken + relayer)。
  2. 定义六项必检条件,逐一用测试证明"缺了它会怎样"(均在本地链上): - 来源链 ID:不校验 → 测试链上的锁定事件被链 B 采信; - 来源合约地址:不校验 → 任何合约 emit 同名事件都被采信; - 消息唯一性(nonce/messageId):不去重 → 同一消息被处理两次,多铸一份; - 目标链 ID:不校验 → 发给链 C 的消息在链 B 被执行; - 签名者集合与阈值:1-of-N → 单点即可铸币; - 金额与速率上限:无限制 → 单笔即可掏空储备。
  3. 每项写一个 forge test,断言"链 B 的 wrapped 总量 ≠ 链 A 的锁定总量" → 失衡即缺陷成立。
  4. 逐项修复,重跑全部测试 → 所有失衡断言失败(即不变量恒成立)。
  5. 实现核心不变量监控:
// 桥的健康检查:任何时刻都必须成立
const locked = await vaultA.read.totalLocked();
const minted = await wrappedB.read.totalSupply();
if (locked !== minted) alert('BRIDGE IMBALANCE');   // 失衡即刻告警并暂停
  1. 加入运维层防御:金额上限、24 小时提款速率限制、异常自动 pause、多签热更新。
  2. 产出"桥安全评估问卷":谁验证?几人验证?验证的是什么(签名/轻客户端/欺诈证明)?失衡如何被发现?多久能暂停?

验证命令

forge test --match-contract BridgeTest -vv
node invariant.mjs    # 期望持续输出 locked == minted

完成标准:产出六项必检清单 + 一个可运行的不变量监控脚本。

常见错误与排查:只在 relayer(链下)做校验 → 链下代码可被绕过,关键校验必须在链上合约里;nonce 用全局单调递增 → 多链场景需 (srcChain, nonce) 组合。

安全提示:桥的资金集中度极高,是攻击的高价值目标。作为用户,不要在桥合约上长期停留大额资金;作为开发者,"链上不变量 + 自动暂停"是最后一道也是最有效的一道防线

重置方法:重启两个 anvil,删除教学合约。

进阶挑战:实现"每日提款上限 + 超限进入 24 小时延迟队列",模拟异常发生时人工干预的时间窗口。

思考题:把"信任 N 个签名者"换成"信任对方链的共识"(轻客户端)成本极高。这个成本差是否解释了为什么大多数桥选择了前者?

对应章节:第 22 章 跨链与互操作


实验45:恶意签名识别

等级:进阶 适合角色:所有人(安全关键) 风险等级:无风险(本地解析,不广播任何签名) 预计时间:90 分钟 前置知识:基础卷实验 13、14

安全边界:本实验只对本地构造的样本做离线解析不连接任何真实站点、不广播任何签名、不生成可用于欺骗他人的页面。 目的是训练"看懂再签"的能力。

学习目标:建立一个能对任意签名请求输出风险评分的解析器,覆盖 personal_sign、EIP-712、交易 calldata 三类。

场景:钱包弹出一个窗口,标题写着"验证钱包所有权",下面是一段结构化 JSON。你有 5 秒钟决定签还是不签。

环境要求:Node.js + viem。

初始化命令

mkdir -p ~/web3-labs/sec/lab45 && cd ~/web3-labs/sec/lab45 && npm init -y && npm i viem

操作步骤 / 每步预期结果

  1. 收集本地构造的样本集(自己写,共 10 个):正常登录签名(SIWE)、正常限额授权、无限额度 approve、setApprovalForAll(true)、NFT 市场零价挂单、permit 给陌生 spender、increaseAllowance、代持转移(transferFrom 自己 → 陌生地址)、伪装成登录实则含交易意图的 typed data、域名与 verifyingContract 不匹配的样本。
  2. 实现三类解析器:
import { hexToString, decodeFunctionData, parseAbi } from 'viem';

// A) personal_sign:能转成可读文本就基本是低风险
const readable = (hex) => { try { return hexToString(hex); } catch { return null; } };

// B) EIP-712:关键字段清单
const check712 = (td) => ({
  domain: td.domain.name, chainId: td.domain.chainId,
  verifyingContract: td.domain.verifyingContract,
  type: td.primaryType, message: td.message,
});

// C) calldata:解出函数名与参数
const decode = (data, abi) => decodeFunctionData({ abi: parseAbi(abi), data });
  1. 实现评分规则: - 额度为 2^256-1(无限)→ +4 - setApprovalForAll(_, true)+4 - spender / operator 是新地址或非合约 → +3 - verifyingContract 与你所在网站的官方合约不一致 → +4 - chainId 与当前网络不符 → +3 - 无法解析(未知 selector、无 ABI)→ +5(一律拒签) - 挂单价格为 0 或极低 → +5
  2. 跑全部 10 个样本 → 输出风险分与理由,验证 4 个高危样本得分均 ≥ 7。
  3. 制作《签名前五问》卡片:谁在请求?签给哪个合约?授权了什么?上限多少?我能读懂吗?
  4. 演练"读不懂就拒签":对无 ABI 的 selector,用 4 字节数据库查询;查不到即拒。

验证命令

node score.mjs samples/*.json
# 期望:无限授权/ApprovalForAll/零价挂单三类样本得分最高

完成标准:拿到任意签名请求,能在 2 分钟内给出风险判断和理由。

常见错误与排查:只看钱包显示的标题 → 标题由网站提供,可任意伪造;以为"签名不花 gas 所以无害" → 签名可以是一张可被别人拿去执行的授权书,这正是它比转账更危险的原因。

安全提示签名不需要 gas,不上链,因此没有任何"确认中"的缓冲时间可供你反悔。 遇到任何"验证钱包 / 同步资产 / 解锁账户"话术,一律拒绝。若已误签,立即用实验 40 的方法检查并撤销授权,并把剩余资产转移到新地址。

重置方法:删除目录。

进阶挑战:把解析器做成浏览器扩展的原型(仅本地运行),在钱包弹窗前先展示人类可读的风险摘要。

思考题:钱包本可以默认把所有签名翻译成人话。为什么这件事推进得如此缓慢?

对应章节:第 10 章 交互安全


实验46:本地钓鱼桌面推演

等级:进阶 适合角色:所有人 / 团队安全培训 风险等级:无风险(本地静态页面 + 纸面推演) 预计时间:90 分钟 前置知识:基础卷实验 15、实验 45

安全边界(务必先读):本实验为桌面推演(tabletop exercise),全部在纸面与 localhost 静态页面上进行。 不访问、不收集、不传播任何真实钓鱼网址;不制作可部署的仿冒站点;不向任何人发送任何测试性钓鱼内容。 所有练习页面只在本机运行,实验结束即删除。目的是训练识别与团队响应

学习目标:通过角色扮演演练完整的钓鱼攻防链条(诱饵 → 接触 → 说服 → 执行 → 事后),产出团队级防御清单。

场景:团队里有人在 Discord 收到"官方"私信,说有紧急空投需要立即领取。

环境要求:浏览器 + 本地静态服务器 + 白板/文档。

初始化命令

mkdir -p ~/web3-labs/sec/lab46 && cd ~/web3-labs/sec/lab46
python3 -m http.server 8080     # 仅 localhost,不对外暴露

操作步骤 / 每步预期结果

  1. 推演角色分配:受害者、观察者、响应负责人。(不设"攻击者"角色执行真实操作,攻击侧仅在纸面描述其惯用手法。)
  2. 诱饵盘点(纸面):列出常见诱饵形态——空投、限量 mint、账户异常告警、客服主动联系、招聘任务、"帮我测试一下我的 DApp"、伪造的安全通知。
  3. 接触渠道盘点:私信、被入侵的官方账号、搜索广告位、置顶评论、日历邀请、二维码。
  4. 本地页面练习:写两个本地 HTML(safe.html / lookalike.html),后者刻意包含同形字符标题、倒计时压力、"验证钱包"按钮(按钮只 console.log,不连接任何钱包)→ 让"受害者"角色在 30 秒内判断。
  5. 决策点标注:在推演流程图上标出受害者本可以中止的 5 个节点(收到私信时、点击链接时、连接钱包时、阅读签名时、确认时)→ 每个节点都有一个简单动作能阻断整条链
  6. 响应演练:假设已签名,计时演练——发现(多久?)→ 转移剩余资产 → 撤销授权 → 通知团队 → 公告 → 复盘。目标是 15 分钟内完成前三步。
  7. 产出团队规范:官方链接只从固定收藏进入;任何"紧急"要求必须二次确认;高价值操作必须双人复核;团队内部禁止通过私信发送任何链接。

验证命令

node -e "console.log([...'аdmin'].map(c=>c.codePointAt(0).toString(16)))"
# 输出首字符 430(西里尔字母)→ 证明同形字符肉眼不可辨

完成标准:产出一份团队钓鱼防御规范和一份 15 分钟应急响应清单。

常见错误与排查:把推演做成"技术演示"忽略人的因素 → 钓鱼成功的关键从来是心理而非技术;只培训一次 → 应定期重演,尤其在新人入职后。

安全提示(重要): - 绝不为"验证"而访问真实可疑链接。 遇到疑似钓鱼,正确动作是不点击、截图留证、向官方举报。 - 绝不把本实验的任何页面部署到公网或发送给他人,即使是"为了测试同事的警觉性"——未经授权的钓鱼测试可能违法且严重损害信任。 - 若团队确需做钓鱼演练,必须由管理层授权、由专业平台执行、事后统一告知。

重置方法:停掉本地服务器并 rm -rf ~/web3-labs/sec/lab46

进阶挑战:为团队写一份"官方渠道清单"文档,包含所有正确域名、官方账号、公告渠道,并在每次入职时分发。

思考题:为什么技术能力强的人也会中招?"我不会上当"这个信念本身是不是一个风险因素?

对应章节:第 11 章 常见骗局与防御


实验47:公开历史资金流只读追踪

等级:高级 适合角色:分析师 / 安全研究者 / 记者 风险等级:无风险(纯只读,固定历史数据预计时间:110 分钟 前置知识:开发卷实验 33

安全边界(务必先读):本实验只读取公开链上数据与已公开报道的历史事件资料不发起任何交易,不连接钱包,不与任何合约交互,不接触任何私钥。 分析对象限定为已被广泛公开报道的历史事件相关地址或公开的协议地址不对个人做身份指认,不进行人肉搜索,不发布未经证实的关联结论。

学习目标:掌握资金流追踪的完整方法论(起点确认 → 逐跳展开 → 分叉处理 → 终点分类 → 置信度标注),并理解其固有局限。

场景:一笔大额资金在事故后开始移动。你要在不做任何猜测的前提下,说清它去了哪。

环境要求:Node.js + viem + 只读公共 RPC;本地链用于方法预演。

初始化命令

mkdir -p ~/web3-labs/sec/lab47 && cd ~/web3-labs/sec/lab47 && npm init -y && npm i viem
printf "hop,from,to,asset,amount,txhash,timestamp,note,confidence\n" > flow.csv

操作步骤 / 每步预期结果

  1. 本地预演:先在 anvil 上造一条 6 跳、含 2 处分叉的资金路径,用它调试你的追踪脚本 → 确保脚本正确后再用于只读真实数据。
  2. 起点确认:确定初始地址与初始时间,记录当时余额快照。所有后续结论都相对于这个起点。
  3. 逐跳展开:对每个地址拉取 ETH 转账 + ERC-20 Transfer 事件,按时间排序:
const logs = await client.getLogs({
  event: parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)'),
  args: { from: target }, fromBlock: START, toBlock: END
});
// 只读查询;不发送任何交易
  1. 分叉处理:一笔资金拆成多笔时,按金额降序保留前 N 条路径,其余标注为"未展开"→ 诚实记录你没追的部分
  2. 终点分类:把每条路径的终点归类——中心化交易所充值地址、跨链桥、混币器、DEX、长期静止地址、仍在流动。
  3. 置信度标注:每一跳标注 high(直接转账)/ medium(时间与金额高度吻合但经过合约)/ low(推断)→ 报告中必须保留这一列
  4. 交叉验证:把你的结论与公开报道对比,找出差异并解释原因(可能是对方也用了启发式)。
  5. 产出报告:起点 → 主要路径图 → 终点分布表 → 未展开部分说明 → 置信度分布 → 明确的局限声明

验证命令

node trace.mjs --start <公开地址> --hops 6 --readonly
wc -l flow.csv                      # 每一跳一行,含置信度
grep -c "low" flow.csv              # 低置信度占比应在报告中披露

完成标准:产出一份含路径图、终点分类、置信度标注和局限声明的追踪报告。

常见错误与排查:把合约中转当成"实控人转移" → 需区分 EOA 与合约;混币器之后继续"追踪" → 混币器输出与输入的关联在多数情况下不可靠,应明确标注为追踪终止点;忽略同一笔资金被多次统计 → 需去重。

安全提示(重要): - 链上分析结论是概率性的,不是证据。表述必须是"地址 X 与地址 Y 存在资金关联",而非"某人转移了资金"。 - 错误的公开指认会造成严重的现实伤害,并可能承担法律责任。 - 涉及执法或司法用途的追踪应由专业机构在合法授权下进行。

重置方法:删除目录(报告建议归档保留)。

进阶挑战:把 flow.csv 转成 DOT 图,用不同颜色标注置信度等级,生成可视化资金流向图。

思考题:如果资金流是完全公开的,为什么盗窃仍然频繁发生?"可追踪"与"可追回"之间隔着什么?

对应章节:第 26 章 链上监控与风控


实验48:项目方事故响应演练

等级:高级 适合角色:团队负责人 / 运维 / 安全 风险等级:无风险(本地链演练 + 桌面推演) 预计时间:120 分钟 前置知识:开发卷实验 37、38;实验 44

安全边界:本实验在本地链上你自己部署的教学协议上演练,并结合纸面推演。 不涉及任何真实协议,不模拟对他人系统的攻击。 产出是响应手册(runbook)。

学习目标:把"事故发生后的前 60 分钟"变成一套可演练、可计时、有明确责任人的标准流程。

场景:凌晨 3 点,监控告警:协议金库余额异常下降。你是值班负责人。

环境要求:anvil + 本地部署的教学协议(含 pause、多签 admin、监控脚本)+ 团队协作工具(可用文档模拟)。

初始化命令

mkdir -p ~/web3-labs/sec/lab48 && cd ~/web3-labs/sec/lab48 && forge init --no-git . && anvil
printf "# 事故响应 Runbook\n\n## T+0 检测\n## T+5 确认\n## T+10 止血\n## T+30 沟通\n## T+60 稳定\n## T+24h 复盘\n" > runbook.md

操作步骤 / 每步预期结果

  1. 搭建演练环境:本地部署带 pause() 的教学协议(// 仅本地教学,请勿部署到主网)+ 2/3 多签 admin + 实验 44 的不变量监控脚本。
  2. 注入故障(本地):由演练主持人手动执行一个"异常提款"(用 admin 权限直接转出),触发不变量告警 → 这是模拟故障,不是攻击演示
  3. T+0 检测:告警响起,计时开始。记录:谁收到告警?多久看到?
  4. T+5 确认:值班人查询链上状态,确认是真异常还是误报(用实验 39 的规则 + 实验 47 的只读查询)→ 产出一句话事实描述。
  5. T+10 止血:召集多签签名者执行 pause()计时:从决定到链上生效用了多久? 这个数字通常是团队最大的短板。
  6. T+30 沟通:起草对外公告(事实 + 已采取措施 + 用户该做什么 + 下次更新时间),不猜测原因、不承诺赔偿。同步通知集成方与交易所。
  7. T+60 稳定:评估影响范围、冻结相关权限、准备取证数据(交易 hash、区块高度、状态快照)。
  8. T+24h 复盘:套用开发卷实验 38 的模板产出报告。
  9. 计时复盘:把每个阶段的实际耗时填入表格,找出瓶颈(通常是"联系到足够多的多签签名者")。
  10. 产出 runbook:明确每个阶段的责任人、备份责任人、联系方式、决策权限、公告模板。

验证命令

cast call <协议> "paused()(bool)" --rpc-url http://127.0.0.1:8545   # 止血后应为 true
node invariant.mjs                                                   # 暂停后不变量不再恶化

完成标准:完成一次全流程演练,产出带责任人和时限的 runbook,且 pause 生效时间 < 15 分钟。

常见错误与排查:只有一个人知道怎么 pause → 单点故障,必须有备份并演练;多签签名者时区集中 → 需跨时区分布;公告写得像法律文书 → 用户需要的是"我该做什么"。

安全提示暂停能力本身是一把双刃剑——它是最有效的止血手段,也是一个中心化权限(呼应开发卷实验 23/37)。合理的做法是:pause 权限门槛低(快速止血),unpause 与资金移动权限门槛高(多签 + timelock)。

重置方法:重启 anvil;runbook 建议保留并定期更新。

进阶挑战:做一次"无预告"演练——主持人在随机时间注入故障,测量真实响应时间。

思考题:如果协议完全没有暂停权限(真正的不可变),事故响应还剩下什么手段?这种取舍你会怎么选?

对应章节:第 25 章 安全事故与行业教训


实验49:交易所挤兑推演

等级:高级 适合角色:所有人 / 分析师 / 风控 风险等级:无风险(纯建模 + 固定历史数据) 预计时间:100 分钟 前置知识:开发卷实验 34;实验 47

安全边界:本实验为建模与桌面推演,使用自建模型和已公开的历史事件资料不针对任何在运营的机构做偿付能力判断,不构成投资建议,不发起任何真实操作。

学习目标:建立中心化托管机构的挤兑模型,理解"部分准备金 + 信息不对称 + 社交媒体加速"如何在数日内摧毁一家机构,并掌握用户侧的可验证性工具与局限。

场景:一家托管平台的储备构成被质疑。24 小时内提款请求激增 10 倍。

环境要求:Node.js 或电子表格。

初始化命令

mkdir -p ~/web3-labs/sec/lab49 && cd ~/web3-labs/sec/lab49 && npm init -y

操作步骤 / 每步预期结果

  1. 建立资产负债模型:
let liabilities = 1e9;                 // 用户存款(负债)
let liquid = 3e8;                      // 高流动性储备(现金/稳定币)
let illiquid = 6e8;                    // 低流动性资产(自家代币、锁仓、长期投资)
let illiquidHaircut = 0.4;             // 抛售折价 40%

function day(withdrawRate) {
  const req = liabilities * withdrawRate;
  const fromLiquid = Math.min(liquid, req);
  liquid -= fromLiquid;
  let rest = req - fromLiquid;
  if (rest > 0) {                       // 被迫抛售低流动性资产
    const need = rest / (1 - illiquidHaircut);
    illiquid -= need;
    illiquidHaircut = Math.min(0.9, illiquidHaircut + 0.1);   // 抛售加剧折价
  }
  liabilities -= req;
  return { liquid, illiquid, solvent: liquid + illiquid * (1 - illiquidHaircut) >= liabilities };
}
  1. 跑基准情形:日提款率 3% → 平稳,5 天后仍可兑付。
  2. 跑压力情形:日提款率 20% → 第 2 天流动性耗尽,第 3 天资不抵债 → 注意:机构在第 1 天从外部看仍完全正常
  3. 加入"反身性":暂停提款的公告本身会把提款率推到 40% → 观察暂停如何加速崩溃而非阻止它。
  4. 加入"自家代币抵押"变量:储备中若有大比例自家发行代币,其价格与机构信誉正相关 → 危机时储备价值同步归零,这是历史上多起事件的共同结构
  5. 用户侧可验证性工具与局限: - 储备证明(Proof of Reserves):能验证"链上有多少资产",不能验证负债,因此不能证明偿付能力; - Merkle 树负债证明:用户可验证自己的余额被计入,但无法验证没有被遗漏的负债; - 完整方案需要储备 + 负债 + 独立审计三者齐备。 - 用只读链上查询检查一个公开的托管地址余额 → 体会"看得见余额,看不见负债"。
  6. 产出用户侧检查清单:储备证明频率与范围、是否有独立审计、储备构成(是否含自家代币)、提款历史是否顺畅、监管与牌照情况、以及最重要的一条——不要把不能承受损失的资产长期放在托管方

验证命令

node bankrun.js --rate 0.20    # 期望第 2-3 天 solvent 变为 false

完成标准:能画出不同提款率下的存活天数曲线,并说清储备证明能证明什么、不能证明什么。

常见错误与排查:把"有储备证明"当成安全保证 → 它只是必要不充分条件;忽略资产的流动性分层 → 账面价值不等于可变现价值。

安全提示"Not your keys, not your coins." 本实验不针对任何具体机构。托管有其便利性(找回、合规、法币通道),自托管有其风险(助记词丢失、操作失误)。合理做法是根据金额与用途分层(呼应基础卷实验 16),而不是极端化任何一方。

重置方法:删除目录。

进阶挑战:加入"存款保险"变量,测算需要多大比例的保险基金才能扛住 30% 的挤兑。

思考题:部分准备金制度在传统银行有央行最后贷款人兜底。加密托管机构的"最后贷款人"是谁?

对应章节:第 28 章 中心化机构风险


实验50:稳定币脱锚推演

等级:高级 适合角色:所有人 / 分析师 / DeFi 使用者 风险等级:无风险(建模 + 本地链模拟 + 固定历史数据) 预计时间:100 分钟 前置知识:开发卷实验 27;实验 49

安全边界:本实验使用自建模型、本地教学合约与已公开的历史资料不针对任何在流通的稳定币做偿付判断,不构成投资建议。

学习目标:把脱锚拆成"触发 → 套利失效 → 传染 → 终局"四个阶段,量化每个阶段的临界点,并建立用户侧的预警指标。

场景:一个稳定币跌到 0.98。这是暂时的流动性问题,还是崩溃的开始?

环境要求:Node.js + 可选 anvil(复用开发卷实验 24 的 AMM)。

初始化命令

mkdir -p ~/web3-labs/sec/lab50 && cd ~/web3-labs/sec/lab50 && npm init -y
anvil    # 可选:用本地 AMM 复现价格冲击

操作步骤 / 每步预期结果

  1. 阶段一:触发。在本地 AMM 的稳定币池中做一笔占池子 20% 的抛售 → 价格跌到 0.97。观察:小额脱锚在深池中会被套利者迅速修复。
  2. 阶段二:套利失效。建模套利者的行为:
// 套利者愿意买入的条件:赎回渠道畅通 且 折价 > 交易成本
function arbWillBuy(price, redeemOpen, gasCost, counterpartyRisk) {
  const profit = 1 - price - gasCost;
  return redeemOpen && profit > counterpartyRisk;    // counterpartyRisk 随恐慌上升
}

→ 当赎回被暂停或对手方风险认知上升,套利者集体退出,价格失去锚定力。这是最关键的转折点。

  1. 阶段三:传染。列出传染路径并逐条建模: - 该稳定币作为抵押品的借贷协议 → 触发大规模清算(复用开发卷实验 26); - 与其配对的 AMM 池 → LP 被动持有全部贬值资产(复用实验 25 的 IL 逻辑); - 集成该稳定币的其他协议 → 连锁暂停; - 使用它作为价格基准的预言机 → 报价失真(复用实验 43)。
  2. 跑传染模拟:初始 5% 脱锚 → 输出各协议受影响规模与清算量。
  3. 阶段四:终局。三种可能:恢复锚定(需外部注资或赎回恢复)、稳定在折价区间、归零。分别给出判别信号。
  4. 用户侧预警指标(可只读查询):赎回渠道是否畅通、储备构成公开程度、主要流动性池的深度与失衡度、大额持有者是否在退出、协议是否已暂停某些功能。
  5. 产出应对预案:不同脱锚幅度下的动作(0.5% 观察、2% 减仓、5% 退出并检查所有相关敞口)。

验证命令

node depeg.js --shock 0.05     # 期望输出传染链条与各环节影响规模
cast call <本地AMM> "getAmountOut(uint256,uint256,uint256)(uint256)" ... --rpc-url http://127.0.0.1:8545

完成标准:能对一次脱锚事件快速判断处于哪个阶段,并说出对应的行动。

常见错误与排查:把"价格回到 1"当成风险解除 → 需确认赎回渠道与储备是否真正修复;只看单一稳定币忽略自己在其他协议的间接敞口 → 需做全仓位敞口盘点。

安全提示:脱锚期间往往伴随大量诈骗("官方救助计划""紧急迁移合约")。任何在危机中出现的、要求你签名或转账的"救助"入口都应视为钓鱼(呼应实验 45、46)。本实验不构成投资建议。

重置方法:删除目录;重启 anvil。

进阶挑战:把你的所有 DeFi 头寸做一次"稳定币敞口穿透"——包括间接持有(LP 份额、借贷抵押、收益凭证)。

思考题:稳定币的锚定依赖套利者。当套利者认为"这次不一样"时,机制就失效了。这算机制缺陷还是人性缺陷?

对应章节:第 19 章 稳定币


实验51:审计报告阅读

等级:进阶 适合角色:所有人 / 投资者 / 开发者 风险等级:无风险(只读文档 + 只读链上验证) 预计时间:90 分钟 前置知识:开发卷实验 23、37

安全边界:本实验只阅读公开的审计报告并做只读链上验证,不发起任何交易。

学习目标:学会读审计报告的"隐藏信息"——范围、时间点、未修复项、审计方立场,理解"通过审计"不等于"安全"。

场景:项目方在首页放了三个审计机构的 logo。你需要 90 分钟判断这意味着什么。

环境要求:浏览器(只读)+ cast(只读查询)。

初始化命令

mkdir -p ~/web3-labs/sec/lab51 && cd ~/web3-labs/sec/lab51
printf "维度,内容,风险信号\n" > audit-review.csv

操作步骤 / 每步预期结果

  1. 找到报告原文。只有 logo 没有可下载报告 → 第一个红旗,记录。
  2. 核对范围(Scope):报告审的是哪几个文件?哪个 commit hash?→ 与当前部署的合约对比。范围外的合约完全未被审计,这一点常被忽略。
  3. 核对时间线:审计完成日期 vs 合约最后一次升级日期(用 Upgraded 事件查,复用开发卷实验 37)→ 升级晚于审计 = 审计已部分失效
  4. 统计发现项:按严重程度分类(Critical / High / Medium / Low / Informational),统计每类的"已修复 / 已确认但未修复 / 有争议"状态。
  5. 重点读"未修复"与"项目方回应":项目方写"acknowledged, will not fix"的高危项,就是明晃晃写在报告里的已知风险。这是全篇最有价值的部分。
  6. 读免责声明:几乎所有报告都会写"本报告不保证代码无漏洞""不覆盖经济模型/运营/密钥管理"→ 明确审计的边界。
  7. 只读链上验证:报告里说"owner 已转为多签"→ 实际查一下。
cast call <合约> "owner()(address)" --rpc-url <只读RPC>
cast code <owner地址> --rpc-url <只读RPC>       # 非 0x 说明是合约(可能是多签),0x 说明是 EOA

→ 文档与链上不一致是最强的风险信号。

  1. 填完 audit-review.csv,输出结论:审计覆盖度、时效性、未修复高危数、文档与链上一致性。

验证命令

awk -F, 'NR>1 {print $3}' audit-review.csv | sort | uniq -c    # 统计风险信号数量

完成标准:产出一份 1 页审计评估,明确回答"这份审计覆盖了什么、没覆盖什么、现在还有效吗"。

常见错误与排查:只看"发现 0 个 Critical"就放心 → 可能是范围太窄;把 Informational 项当噪音 → 其中常含设计缺陷提示;不同审计机构标准差异大 → 应看具体发现而非机构名气。

安全提示审计是"某个时间点、某个范围内、由某些人做的抽样检查",不是安全证明。 历史上大量被攻破的协议都通过了审计。审计报告应与权限审查(开发卷实验 37)、事故复盘(实验 38)、监控(实验 39)配合使用。本实验不构成投资建议。

重置方法:删除目录(评估报告建议归档)。

进阶挑战:横向对比同一项目的多份审计报告,找出后一份发现了前一份漏掉的问题——这直接说明单份审计的局限。

思考题:审计机构由项目方付费。这个激励结构会产生什么偏差?有什么替代方案(漏洞赏金、竞赛式审计、形式化验证)?

对应章节:第 29 章 审计与安全实践


实验52:完整安全事故报告

等级:高级 适合角色:安全研究者 / 团队负责人 / 分析师 风险等级:无风险(固定历史数据 + 只读公开信息) 预计时间:150 分钟(可分两次) 前置知识:本卷实验 39–51;开发卷实验 38

安全边界(务必先读):本实验只使用已公开报道的历史事件资料与只读公开链上数据不复现攻击、不编写攻击代码、不推导可用于攻击的步骤、不做未经证实的身份指认。 报告的产出是防御清单、响应改进与行业教训。 若需在本地演示某类缺陷原理,一律使用本卷实验 41–44 中已标注 // 仅本地教学,请勿部署到主网 的教学合约。

学习目标:整合本卷全部技能,独立完成一份可交付的完整安全事故报告,达到能提交给团队或发布为公开分析的质量。

场景:你要为一次公开的历史事故写一份 3000 字的分析,读者包括工程师、管理层和普通用户。

环境要求:浏览器(只读)+ 本卷实验 39/40/47 的脚本 + 开发卷实验 33/37 的脚本。

初始化命令

mkdir -p ~/web3-labs/sec/lab52 && cd ~/web3-labs/sec/lab52
cat > outline.md <<'EOF'
# 事故分析报告
## 0 执行摘要(200字,给管理层)
## 1 背景:协议做什么、规模多大
## 2 事实时间线(每条带来源)
## 3 根本原因(技术/经济/流程/密钥/第三方)
## 4 传导与影响范围
## 5 资金流向(只读追踪,带置信度)
## 6 响应过程评价(时间线 + 做对/做错)
## 7 教训与可执行检查项
## 8 对用户的建议
## 9 局限与未知
## 附录 来源清单
EOF

操作步骤 / 每步预期结果

  1. 选题:选择一个官方已发布 post-mortem 的历史事件,资料完整、争议少。记录你选它的理由。
  2. 第 1 节 背景:用只读查询确认协议的合约地址、部署时间、事发前 TVL 规模(复用开发卷实验 34)。
  3. 第 2 节 时间线:从官方公告、审计报告、公开报道整理,精确到小时,每条注明来源 URL。用只读链上数据交叉验证关键时间点。
  4. 第 3 节 根因:归入五类之一,并说明"哪一个环节的哪一条防御缺失"。若涉及代码缺陷,用本卷实验 41/42 的教学合约说明该类缺陷的一般形态——描述模式,不给攻击代码
  5. 第 4 节 影响:直接损失、二级影响(依赖该协议的其他项目)、用户分布。
  6. 第 5 节 资金流向:用实验 47 的方法只读追踪,产出路径图与终点分类,保留置信度标注与未展开说明
  7. 第 6 节 响应评价:对照实验 48 的 runbook 阶段,逐段评价耗时与决策质量。
  8. 第 7 节 检查项:把每条教训写成可执行的一句话(如"所有外部调用后禁止写状态""预言机必须多源 + staleness check""admin 必须多签 + timelock")。
  9. 第 8 节 用户建议:普通用户在类似情况下能做什么(撤销授权、分散托管、关注哪些信号)。
  10. 第 9 节 局限:明确写出你没能确认的部分。一份诚实标注未知的报告比一份看似完整的报告更可信。
  11. 交叉审阅:请一位同伴按"每个事实能否溯源"逐条检查。

验证命令

grep -c "http" outline.md          # 来源数量,建议 >= 10
wc -w report.md                    # 目标 3000 字左右
grep -c "置信度\|confidence" report.md   # 资金流部分必须有置信度标注

完成标准:报告含全部 10 个章节,每条事实可溯源,检查项可直接执行,且明确标注了局限。

常见错误与排查:把大量篇幅花在"攻击有多精巧"→ 读者需要的是防御;用传言填补空白 → 宁可写"未知";只写技术不写流程 → 多数事故的放大原因是响应流程问题。

安全提示报告中不得包含任何可复用的攻击代码、calldata 或利用步骤。 涉及个人的推断必须标注为推断且避免指名。若在写作过程中发现某个尚未修复的真实漏洞,应立即停止公开写作并走负责任披露流程。

重置方法:报告建议归档保留,作为个人安全知识库。

进阶挑战:把同一事故写成三个版本——给工程师(技术细节)、给管理层(风险与决策)、给用户(我该做什么),体会同一事实的不同表达需求。

思考题:安全事故报告的最大价值是防止重演。但同类漏洞仍反复出现。是知识传播的问题,还是激励结构的问题?

对应章节:第 25 章 安全事故与行业教训


实验53:多签权限审查

等级:高级 适合角色:团队负责人 / 尽调 / 安全 风险等级:无风险(本地部署演练 + 只读查询) 预计时间:110 分钟 前置知识:开发卷实验 37;实验 48

安全边界:多签的部署、配置与操作演练全部在本地链上你自己部署的多签合约上进行。 对真实多签只做只读查询(owners、threshold、历史交易),不发起任何提案、不签署任何交易

学习目标:掌握多签的完整审查方法——阈值合理性、签名者独立性、密钥分布、执行流程、以及"多签剧场"的识别。

场景:一个协议宣称"资金由 5/9 多签管理"。你需要判断这句话到底提供了多少安全性。

环境要求:Foundry + anvil;本地部署一个 Safe 式多签或自写教学多签。

初始化命令

mkdir -p ~/web3-labs/sec/lab53 && cd ~/web3-labs/sec/lab53 && forge init --no-git . && anvil

操作步骤 / 每步预期结果

  1. 本地部署一个 M-of-N 多签教学合约(// 仅本地教学,请勿部署到主网),配置 5 个 owner、阈值 3。
  2. 走通完整流程:提交提案 → 收集 3 个签名 → 执行 → 观察不足 3 个签名时执行 revert。
  3. 阈值实验:分别改成 1/5、3/5、5/5,测试: - 1/5 → 等同单签,任何一把私钥泄露即失守; - 5/5 → 任何一人失联即永久锁死(演示:删掉一个私钥,资金再也取不出); - 3/5 → 容忍 2 人失联,需 3 人合谋才能作恶 → 理解阈值是"可用性 vs 安全性"的权衡点。
  4. 签名者独立性审查(这是最容易造假的一环)。对本地演练地址和真实地址(只读)都做这套检查: - 5 个 owner 地址的资金是否都来自同一个源地址?(用实验 47 的只读追踪) - 创建时间是否集中在同一天同一小时? - 链上活动是否高度雷同(相同 gas 价格、相同交互协议、相同时间段)? - 若答案都是"是",很可能是同一人控制的多个地址 —— 这就是"多签剧场"。
  5. 密钥分布审查(纸面):签名者是否分布在不同组织、不同地理位置、不同时区?是否都用硬件钱包?是否有人同时是 owner 和运维?
  6. 执行流程审查:签名前是否解码 calldata(复用实验 45)?是否有内部审批记录?是否有 timelock 兜底?
  7. 只读实操:查询一个公开协议的多签配置。
cast call <多签地址> "getOwners()(address[])" --rpc-url <只读RPC>
cast call <多签地址> "getThreshold()(uint256)" --rpc-url <只读RPC>
cast code <每个owner地址> --rpc-url <只读RPC>     # 判断 owner 是 EOA 还是合约
  1. 产出审查报告:阈值 / 签名者数 / 独立性评分 / 密钥类型 / 是否有 timelock / "最少几个人合谋即可转走全部资金" 这个终极答案。

验证命令

forge test --match-contract MultisigTest -vv
# 期望:不足阈值时执行 revert;达到阈值时成功
cast call <本地多签> "getThreshold()(uint256)" --rpc-url http://127.0.0.1:8545

完成标准:能对任意多签回答"最少几人合谋可转走资金""最多几人失联仍可运作",并给出独立性评分。

常见错误与排查:只看 M/N 数字不看签名者独立性 → 9 个地址属于同一人的 5/9 多签等于单签;把多签当成万能解 → 多签防的是单点密钥泄露,防不了签名者被集体钓鱼(历史上有多起签名者被诱导签署伪装 calldata 的事件)。

安全提示(重要): - 多签的安全性不在数字,而在签名者的独立性与签名流程的严谨性。 - 签名者必须在签名前独立解码并核对 calldata(实验 45),不能仅凭发起人的口头描述。 - 关键的高价值多签应配合 timelock,为社区留出发现异常的窗口。 - 对真实多签只做只读查询,绝不发起或签署任何交易

重置方法:重启 anvil,删除本地多签合约。

进阶挑战:设计一套多签操作规范,包含:提案模板、calldata 独立解码要求、跨渠道二次确认、签名者轮换与演练周期。

思考题:多签把"信任一个人"变成"信任 M 个人"。当这 M 个人都在同一个 Telegram 群里、由同一个人协调时,信任真的被分散了吗?


本卷小结

主题 实验
交易与授权风险识别 39、40、45
合约漏洞识别与修复(本地教学合约) 41、42、43、44
社会工程与团队防御 46、48
只读追踪与系统性风险推演 47、49、50
审计、报告与权限治理 51、52、53

安全实验数:15 个(实验 39–53)

全卷复述一遍红线

  1. 环境限定:本地链 / 故意设计的教学合约 / 浏览器本地模拟 / 固定历史数据 / 只读公开链上数据。四者之外一律不做。
  2. 教学合约标注:所有故意写成脆弱版本的合约,源码首行必须写 // 仅本地教学,请勿部署到主网,实验结束即删除,绝不进入任何部署脚本。
  3. 产出限定:漏洞类实验只产出"识别方法"与"修复方案",绝不产出武器化代码、可复制的恶意交易、绕过真实权限或盗取资产的步骤
  4. 只读原则:涉及真实链的一切操作只查询、不签名、不连接钱包、不发起交易。
  5. 不指认原则:链上分析结论是概率性的,表述为"存在资金关联",绝不做身份指认。
  6. 负责任披露:发现真实漏洞先私下报告项目方或走漏洞赏金流程,修复公告前不公开、不利用。
  7. 免责:本卷所有内容用于安全教育,不构成投资建议,也不构成法律建议。

至此,三卷共 53 个实验(基础卷 1–17、开发与 DeFi 卷 18–38、安全卷 39–53)构成完整的 Web3 实操路径。