实操实验室·开发与 DeFi 卷

本卷共 21 个实验(实验 18–38),从"部署第一个合约"一路走到"协议分析与事故复盘"。

默认技术栈:本地 EVM 链(Anvil / Hardhat 本地节点)、Solidity 0.8.x、Foundry(forge/cast)、viem 或 ethers.js、Node.js 18+,少量 Python / HTML。 公共测试网与主网只读查询为可选增强项,本地链是默认环境。

通用安全红线(贯穿全卷): - 本卷所有"漏洞模拟""操纵模拟"一律运行在本地链 + 自己部署的故意设计的教学合约上。 - 不提供任何可攻击真实协议的武器化代码,不提供可直接复制的恶意交易,不提供绕过真实权限或盗取资产的步骤。 - 所有教学漏洞合约必须在源码首行标注:// 仅本地教学,请勿部署到主网。 - 涉及真实链的部分一律为只读查询公开数据,不发起任何写交易。

环境准备(本卷共用)

mkdir -p ~/web3-labs/dev && cd ~/web3-labs/dev
forge init --no-git .          # 做什么:生成 Foundry 工程骨架(src/ test/ script/)
# 正常输出:Initialized forge project
# 失败排查:目录非空 → 加 --force;forge 未安装 → 重跑 foundryup
forge install OpenZeppelin/openzeppelin-contracts --no-git
anvil                           # 另一个终端常驻

实验18:部署第一个智能合约

等级:入门(开发) 适合角色:开发者 / 想理解合约的产品经理 风险等级:无风险(本地链) 预计时间:60 分钟 前置知识:基础卷实验 7、8 学习目标:完整走一遍"写合约 → 编译 → 部署 → 读 → 写"的开发闭环,理解合约地址、ABI、字节码、存储槽这四个概念。

场景:智能合约不是"合同",而是一段部署后任何人都能调用、你自己也无法随意修改的程序。

环境要求:Foundry + anvil。

初始化命令

cd ~/web3-labs/dev && anvil     # 另开终端

操作步骤 / 每步预期结果

  1. src/Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Counter {
    uint256 public number;                       // public 自动生成 getter
    event Increased(address indexed by, uint256 newValue);

    function increment() public {
        number += 1;
        emit Increased(msg.sender, number);
    }
}
  1. forge build → 输出 Compiler run successfulout/Counter.sol/Counter.json 中含 abi 与 bytecode。
  2. 部署:
forge create src/Counter.sol:Counter \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
  --rpc-url http://127.0.0.1:8545
# 做什么:把字节码作为一笔 to=null 的交易发出去
# 正常输出:Deployer / Deployed to: 0x... / Transaction hash: 0x...
# 失败排查:connection refused → anvil 没起;insufficient funds → 私钥不是 anvil 账户
  1. 读:cast call <地址> "number()(uint256)" → 返回 0(读是免费的,不上链)。
  2. 写:cast send <地址> "increment()" --private-key <k> → 回执 status 1,gasUsed 约 4 万。
  3. 再读 → 返回 1。用 cast storage <地址> 0 直接读存储槽 0 → 同样是 1,理解"public 变量本质是存储槽"。

验证命令

cast call <地址> "number()(uint256)" --rpc-url http://127.0.0.1:8545   # 期望 1
cast code <地址> --rpc-url http://127.0.0.1:8545 | head -c 20          # 非 0x 说明合约已部署

完成标准:能解释 call 与 send 的区别(是否上链、是否花 gas、是否改状态)。

常见错误与排查Compiler version mismatch → 在 foundry.toml 固定 solc_version;调用返回空 → 函数签名写错,签名必须精确到参数类型。

安全提示:合约一旦部署,代码不可更改(除非用代理模式)。上主网前必须完成测试与审计。本实验的私钥是 anvil 公开测试私钥,绝不可用于任何真实网络

重置方法:重启 anvil,合约消失;forge clean 清编译缓存。

进阶挑战:写一个 decrement() 并让它在 number 为 0 时 revert,观察 revert 的 gas 退还行为。

思考题:如果部署后发现逻辑写错了,你有哪些补救手段?每种手段各引入了什么新的信任假设?

对应章节:第 13 章 智能合约入门


实验19:创建测试 ERC-20

等级:入门(开发) 适合角色:开发者 / 分析师 风险等级:无风险(本地链,代币无价值) 预计时间:70 分钟 前置知识:实验 18 学习目标:亲手实现 ERC-20 的六个核心函数,理解"代币只是一张合约里的余额表",并识别"代币标准可以被恶意改写"。

场景:所谓"发币",本质上就是部署一份维护余额映射的合约。任何人都能发,发币本身不代表任何价值。

环境要求:Foundry + OpenZeppelin + anvil。

初始化命令

cd ~/web3-labs/dev
forge install OpenZeppelin/openzeppelin-contracts --no-git

操作步骤 / 每步预期结果

  1. 用 OpenZeppelin 快速版:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

contract LabToken is ERC20 {
    constructor() ERC20("Lab Token", "LAB") {
        _mint(msg.sender, 1_000_000 ether);
    }
}
  1. 部署 → 得到地址;cast call <token> "totalSupply()(uint256)" 返回 1e24。
  2. transfer 给账户 1 → balanceOf 双方变化正确,且 Transfer 事件被 emit。
  3. approve + transferFrom(复习基础卷实验 14)→ 理解授权与转账的分离。
  4. 手写最小版:不用库,自己实现 balanceOf / transfer / approve / allowance / transferFrom / totalSupply 六件套,跑通同样流程 → 你现在知道代币里没有魔法。
  5. 恶意代币识别练习(教学,仅本地):部署一个 transfer 中带 require(!blacklist[msg.sender]) 的教学合约,标注 // 仅本地教学,请勿部署到主网。观察持有者可以买入却无法卖出 → 这就是"貔貅盘"的机制原理。本步骤只演示识别方法,不用于制作骗局。

验证命令

cast call <token> "balanceOf(address)(uint256)" <addr> --rpc-url http://127.0.0.1:8545

完成标准:能读懂任意 ERC-20 源码,并列出三处需要重点审查的地方(mint 权限、transfer 中的额外条件、owner 能力)。

常见错误与排查:小数位混淆 → ERC-20 的 decimals 只是显示约定,链上永远是整数;余额显示为天文数字 → 忘了除以 1e18。

安全提示任何人都能发行名字叫"USDT"的代币。 判断代币真伪只看合约地址,绝不看名字与图标。审查代币时必须检查:是否有无限 mint 权限、transfer 是否含黑名单/税费、owner 是否可暂停。

重置方法:重启 anvil。

进阶挑战:加上转账手续费(如 1% 销毁),然后观察它如何破坏 AMM 的恒定乘积假设——理解为什么很多 DEX 不支持"税费代币"。

思考题:ERC-20 标准没有强制规定"transfer 必须成功转账"。这个宽松性带来了创新,也带来了什么风险?

对应章节:第 14 章 代币标准


实验20:创建测试 NFT

等级:入门(开发) 适合角色:开发者 / 创作者 风险等级:无风险(本地链) 预计时间:70 分钟 前置知识:实验 19 学习目标:实现 ERC-721,理解 tokenURI 与元数据的存储位置,并亲手验证"NFT 上链的通常只是一个链接"。

场景:你花钱买了一张 NFT。链上真正属于你的是什么?图片存在哪?

环境要求:Foundry + OpenZeppelin + anvil。

初始化命令

cd ~/web3-labs/dev && anvil

操作步骤 / 每步预期结果

  1. LabNFT.sol 继承 ERC721,实现 mint(address to, uint256 id)tokenURI
  2. 部署并 mint 三个 → ownerOf(1) 返回你的地址,balanceOf 返回 3。
  3. 设置 tokenURI(1) 返回 https://example.com/1.json关键观察:链上只存了这个字符串,图片和 JSON 都在链下。
  4. 把 tokenURI 指向的服务关掉(用基础卷实验 1 的本地服务器模拟)→ NFT 依然属于你,但"内容"消失了。
  5. 做一个链上 SVG 版本:tokenURI 直接返回 data:application/json;base64,... 内嵌 SVG → 对比两种方案的 gas 成本与持久性。
  6. 演示 setApprovalForAll:授权给某地址后,它可以转走你全部 NFT → 这是 NFT 领域最危险的签名类型(呼应基础卷实验 13)。

验证命令

cast call <nft> "ownerOf(uint256)(address)" 1 --rpc-url http://127.0.0.1:8545
cast call <nft> "tokenURI(uint256)(string)" 1 --rpc-url http://127.0.0.1:8545

完成标准:能说清 NFT 的"所有权"与"内容"分别存在哪,以及各自的可靠性。

常见错误与排查:mint 到零地址 revert → ERC-721 禁止;tokenURI 查询不存在的 id → revert,需先 mint。

安全提示setApprovalForAll 是"整个集合的无限授权"。任何要求你签这个的陌生站点都应高度警惕。定期检查并撤销不用的 approval。

重置方法:重启 anvil。

进阶挑战:把元数据放到 IPFS(本地 ipfs 节点或仅理解 CID 原理),对比 HTTP URL 与内容寻址在"防替换"上的差异。

思考题:如果元数据可以被项目方随时替换,NFT 的"稀缺性"和"确定性"建立在什么之上?

对应章节:第 14 章 代币标准


实验21:构建最小 DApp

等级:进阶(开发) 适合角色:开发者 / 全栈 风险等级:无风险(本地链) 预计时间:90 分钟 前置知识:实验 18、基础卷实验 6 学习目标:把浏览器钱包、前端页面、本地合约三者连起来,理解 DApp 的实际架构:前端是普通网页,"去中心化"只在合约那一层。

场景:一个 DApp 的前端可以托管在任何服务器上,也可以被替换。你需要知道哪部分是可信的、哪部分不是。

环境要求:anvil + 已部署 Counter 合约 + MetaMask 测试钱包 + 静态服务器。

初始化命令

mkdir -p ~/web3-labs/dev/dapp && cd ~/web3-labs/dev/dapp
npm init -y && npm i viem
python3 -m http.server 8080

操作步骤 / 每步预期结果

  1. index.html,核心逻辑:
<button id="connect">连接钱包</button>
<button id="inc">increment</button>
<p id="val">-</p>
<script type="module">
import { createWalletClient, createPublicClient, custom, http } from 'https://esm.sh/viem';
const ABI = [{"name":"number","type":"function","stateMutability":"view","inputs":[],"outputs":[{"type":"uint256"}]},
              {"name":"increment","type":"function","stateMutability":"nonpayable","inputs":[],"outputs":[]}];
const ADDR = '0x...';                       // 换成你部署的地址
const pub = createPublicClient({ transport: http('http://127.0.0.1:8545') });
document.getElementById('connect').onclick = async () => {
  const [acct] = await window.ethereum.request({ method: 'eth_requestAccounts' });
  window.wallet = createWalletClient({ account: acct, transport: custom(window.ethereum) });
  document.getElementById('val').textContent = await pub.readContract({ address: ADDR, abi: ABI, functionName: 'number' });
};
document.getElementById('inc').onclick = async () => {
  await window.wallet.writeContract({ address: ADDR, abi: ABI, functionName: 'increment', chain: null });
};
</script>
  1. 打开页面点"连接钱包" → MetaMask 弹出连接请求,同意后显示当前值。
  2. 点 increment → MetaMask 弹出交易确认(注意与"签名"确认的界面差异)→ 确认后值 +1。
  3. 断开 anvil 再点读取 → 前端报错,页面还在但数据没了 → 理解前端与链的依赖关系。
  4. 关键实验:把 ADDR 改成另一个合约地址,页面文案完全不变 → 你亲手演示了"前端显示的与实际调用的可以不一致",这正是钓鱼 DApp 的原理(呼应基础卷实验 15)。
  5. 加上"显示即将调用的函数名与参数",做一个对用户诚实的 UI。

验证命令

cast call <ADDR> "number()(uint256)" --rpc-url http://127.0.0.1:8545   # 与页面显示一致

完成标准:DApp 能连接、读取、写入;你能指出这套架构里哪些环节是中心化的。

常见错误与排查window.ethereum is undefined → 未装钱包扩展或页面是 file:// 协议,必须用 http 服务;链 ID 不匹配 → MetaMask 切到 31337。

安全提示:DApp 前端通常托管在中心化服务器/CDN 上,前端被劫持是历史上多起大额事故的直接原因。用户侧的防线是"签名前读懂内容"(基础卷实验 13),而不是"信任这个网站看起来很正规"。

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

进阶挑战:加上网络检测——若用户当前不在 31337,显示警告并提供一键切换。

思考题:如果前端可以被替换,"去中心化应用"这个词准确吗?用户如何在不读代码的情况下自保?

对应章节:第 15 章 DApp 架构


实验22:事件和链上日志

等级:进阶(开发) 适合角色:开发者 / 数据分析师 风险等级:无风险 预计时间:70 分钟 前置知识:实验 18、19 学习目标:理解 event/log 的存储结构(topics 与 data)、indexed 的作用、以及为什么"链上分析"几乎全靠事件。

场景:你想统计一个协议一天有多少笔交易、总额多少。直接读合约存储做不到,因为存储只有"当前状态"没有"历史"。

环境要求:anvil + 已部署 LabToken。

初始化命令

cd ~/web3-labs/dev && anvil

操作步骤 / 每步预期结果

  1. 用 LabToken 做 5 笔 transfer → 产生 5 条 Transfer 事件。
  2. cast logs --address <token> --rpc-url http://127.0.0.1:8545 → 看到日志,每条含 topics 数组与 data。
  3. 解析结构:topics[0] 是事件签名哈希 keccak256("Transfer(address,address,uint256)")topics[1]/[2] 是 indexed 的 from/to;data 是非 indexed 的 amount。
cast keccak "Transfer(address,address,uint256)"   # 应等于 topics[0]
  1. 用 viem 按 from 地址过滤:
const logs = await client.getLogs({
  address: TOKEN,
  event: parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)'),
  args: { from: '0xf39F...' }, fromBlock: 0n
});

→ 只返回该地址发出的转账,这就是索引的价值

  1. 写一个非 indexed 版本的事件对比 → 无法按参数过滤,只能全量拉取后自己筛。
  2. 计算成本:indexed 参数比普通参数贵,但最多 3 个;试着超过 3 个 → 编译报错。

验证命令

cast logs --address <token> --from-block 0 --rpc-url http://127.0.0.1:8545 | grep -c "blockNumber"  # 期望 5

完成标准:能设计一个合约的事件方案,说明哪些字段该 indexed 及原因。

常见错误与排查:查不到日志 → fromBlock 默认是 latest,必须显式设 0;事件参数顺序错 → topics 解析全乱,务必以 ABI 为准。

安全提示:事件是"合约自己说的话",合约可以 emit 任何与实际状态无关的事件。做数据分析时,关键数值应以状态查询或转账记录交叉验证,不能只信事件。

重置方法:重启 anvil。

进阶挑战:实现一个简易索引器:监听 Transfer 事件写入 SQLite,然后按地址查询历史——你复现了 The Graph 的核心思路。

思考题:事件不存在合约存储里(不可被合约读取),只存在日志里。这个设计取舍带来了什么好处?

对应章节:第 7 章 链上数据与可验证性


实验23:合约权限检查

等级:进阶 适合角色:开发者 / 分析师 / 投资者 风险等级:无风险(本地部署 + 只读查询) 预计时间:80 分钟 前置知识:实验 18、19 学习目标:建立一套"任何合约上手先查权限"的标准流程,识别 owner 可以做什么、能不能升级、有无时间锁。

场景:一个协议宣称"完全去中心化",但它的合约 owner 是一个 EOA,且可以随时暂停提款、修改费率、无限增发。

环境要求:Foundry + anvil;可选:只读查询公开链数据。

初始化命令

cd ~/web3-labs/dev && anvil

操作步骤 / 每步预期结果

  1. 部署一个"权限齐全"的教学合约(标注 // 仅本地教学,请勿部署到主网),包含:onlyOwner mintpausesetFeeupgradeTo
  2. 用 owner 账户逐一调用 → 全部成功;换普通账户调用 → 全部 revert(Ownable: caller is not the owner)。
  3. 演示影响:owner 调用 pause() 后,所有用户的 transfer 全部失败 → 你的资产被单个私钥冻结。
  4. 建立检查清单,对任意合约逐条回答: - owner/admin 是谁?EOA、多签,还是治理合约? - owner 能否 mint / burn / pause / 修改费率 / 修改地址参数? - 是否可升级(代理模式)?升级由谁触发?有无 timelock? - 是否有紧急提取(rescue / sweep)函数?范围多大?
  5. 实操查询方法(本地合约练手):
cast call <合约> "owner()(address)" --rpc-url http://127.0.0.1:8545
cast storage <代理地址> 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc --rpc-url http://127.0.0.1:8545
# 第二条读的是 EIP-1967 implementation slot,用于查代理指向哪个实现
  1. 部署一个带 TimelockController 的版本,观察"提议 → 等待 48 小时 → 执行"如何给用户留出撤离窗口。

验证命令

cast call <合约> "paused()(bool)" --rpc-url http://127.0.0.1:8545

完成标准:拿到任意合约地址,能在 15 分钟内产出一份权限清单。

常见错误与排查owner() 不存在 → 可能用 AccessControl,改查 hasRole(DEFAULT_ADMIN_ROLE, addr);代理合约读到的字节码是壳 → 必须读 implementation slot。

安全边界:本实验的权限合约由你自己在本地链部署,用于练习"识别"。对真实项目只做只读查询,不发起任何交易,不尝试绕过任何权限。

安全提示:"去中心化"是可验证的事实,不是宣传口径。一个 owner 是单签 EOA 的协议,其安全上限就是那把私钥的安全上限。

重置方法:重启 anvil。

进阶挑战:写一个脚本,输入合约地址自动输出:是否代理、implementation 地址、owner、是否 paused。

思考题:Timelock 保护了用户,也让协议无法快速响应漏洞。这个矛盾有解吗?

对应章节:第 16 章 协议治理与权限


实验24:AMM 模拟器

等级:进阶 适合角色:开发者 / DeFi 使用者 / 分析师 风险等级:无风险(本地链 + 教学合约) 预计时间:90 分钟 前置知识:实验 19 学习目标:从零实现恒定乘积做市(x·y=k),亲手算出价格、滑点、手续费,理解"池子决定价格"而非"订单簿"。

场景:没有买家和卖家撮合,一个数学公式就能定价。你需要知道这个公式在什么时候对你不利。

环境要求:Foundry + anvil;两个教学 ERC-20。

初始化命令

cd ~/web3-labs/dev && anvil
# 部署 TokenA、TokenB 各 1,000,000

操作步骤 / 每步预期结果

  1. 写最小 AMM(教学版):
// 仅本地教学,请勿部署到主网
function getAmountOut(uint256 amountIn, uint256 rIn, uint256 rOut) public pure returns (uint256) {
    uint256 amountInWithFee = amountIn * 997;              // 0.3% 手续费
    return (amountInWithFee * rOut) / (rIn * 1000 + amountInWithFee);
}
  1. 添加流动性 1000 A + 1000 B → k = 1,000,000,价格 1:1。
  2. 换入 10 A → 得到约 9.87 B;价格变成约 1.02 → 观察滑点随交易量非线性增长。
  3. 换入 500 A(占池子 50%)→ 只得到约 332 B,滑点巨大 → 理解"大额交易吃掉自己的价格"。
  4. 用 Python/JS 画出 x·y=k 曲线与不同交易量下的实际成交价 → 曲线越陡损失越大。
  5. 对比池子深度:把流动性放大 100 倍再换 10 A → 滑点几乎消失 → 深度即滑点保护

验证命令

cast call <amm> "getAmountOut(uint256,uint256,uint256)(uint256)" 10e18 1000e18 1000e18 --rpc-url http://127.0.0.1:8545

完成标准:能手算给定池子下的成交价与滑点,并解释为什么大额交易要拆单或走聚合器。

常见错误与排查:整数除法精度丢失 → 先乘后除;k 不守恒 → 检查手续费是否留在池内。

安全提示:低流动性池子里,你自己的交易就能显著改变价格——这既是滑点来源,也是价格操纵的前提(见实验 28)。交易前务必设置滑点上限与 deadline。

重置方法:重启 anvil。

进阶挑战:实现 LP 份额的铸造与赎回(sqrt(x*y) 初始份额),验证按比例赎回时不产生损失。

思考题:AMM 用"永远有报价"换取了"报价可能很差"。在什么市场条件下这个交换是划算的?

对应章节:第 17 章 DeFi 基础机制


实验25:无常损失模拟器

等级:进阶 适合角色:DeFi 使用者 / 分析师 风险等级:无风险(纯计算 + 本地链) 预计时间:70 分钟 前置知识:实验 24 学习目标:量化无常损失(IL),理解它是"相对于持币不动的机会成本",并算出手续费需要多高才能覆盖它。

场景:你提供了流动性,币价涨了,但你的总资产反而不如当初什么都不做。

环境要求:Node.js 或 Python;可选 anvil 复现。

初始化命令

mkdir -p ~/web3-labs/dev/il && cd ~/web3-labs/dev/il && npm init -y

操作步骤 / 每步预期结果

  1. 实现 IL 公式:
// p = 价格变化倍数(如涨到 2 倍则 p=2)
const il = (p) => 2 * Math.sqrt(p) / (1 + p) - 1;   // 结果为负数,表示相对持币的损失比例
[1, 1.25, 1.5, 2, 4, 10, 0.5, 0.25].forEach(p =>
  console.log(`价格${p}倍 → IL ${(il(p)*100).toFixed(2)}%`));
  1. 运行 → 价格 2 倍时 IL ≈ -5.72%,4 倍时 ≈ -20%,10 倍时 ≈ -42.6%,价格不变时为 0
  2. 在本地 AMM 上实物复现:存入 1000A+1000B,然后用大额兑换把价格推到 2 倍,赎回 LP → 对比"一直持币"的价值,差额与公式吻合。
  3. 加入手续费:假设日交易量是池子的 0.5 倍、费率 0.3%,算年化费率收益 → 与 IL 对比得出盈亏平衡点。
  4. 做一张表:不同波动率下的"手续费需要多少才能打平"。
  5. 特例分析:稳定币对(价格几乎不变)IL 极小 → 理解为什么稳定币池 APR 低但受欢迎。

验证命令

node il.js | grep "价格2倍"    # 期望 IL 约 -5.72%

完成标准:能对任意一个 LP 头寸估算"我需要多少手续费才不亏"。

常见错误与排查:把 IL 理解成"一定会亏" → IL 是相对基准的比较,实际盈亏 = 手续费收入 + IL;忘了 IL 在赎回时才实现 → 未赎回时是浮动的。

安全提示:宣传"高 APR 无风险"的流动性挖矿几乎都隐去了 IL 与代币贬值风险。评估 LP 收益时,务必用"以美元计价的总资产"而不是"代币数量"。

重置方法:删除目录。

进阶挑战:模拟集中流动性(Uniswap V3 式区间做市),观察区间外的头寸如何完全变成单边资产、IL 如何被放大。

思考题:"无常"这个名字合适吗?在什么情况下损失会永久化?

对应章节:第 17 章 DeFi 基础机制


实验26:借贷清算模拟器

等级:进阶 适合角色:DeFi 使用者 / 风控 / 开发者 风险等级:无风险(本地教学合约) 预计时间:90 分钟 前置知识:实验 24、25 学习目标:实现超额抵押借贷的核心逻辑(LTV、健康因子、清算阈值、清算奖励),亲手触发一次清算。

场景:你抵押 ETH 借出稳定币。ETH 跌 30%,你的仓位被陌生人以折扣价买走。

环境要求:Foundry + anvil;教学版价格源(可手动设价)。

初始化命令

cd ~/web3-labs/dev && anvil

操作步骤 / 每步预期结果

  1. 部署教学借贷合约(// 仅本地教学,请勿部署到主网),核心:
// 健康因子 = 抵押价值 * 清算阈值 / 借出价值;< 1 可被清算
function healthFactor(address u) public view returns (uint256) {
    uint256 collUsd = collateral[u] * price / 1e18;
    if (debt[u] == 0) return type(uint256).max;
    return (collUsd * LIQ_THRESHOLD / 1e4) * 1e18 / debt[u];
}
  1. 抵押 10 ETH(价格 2000 → 20000 USD),阈值 80%,借出 10000 USD → HF = 1.6,安全。
  2. 手动把价格设为 1500 → 抵押 15000,HF = 1.2,仍安全。
  3. 价格设为 1200 → HF = 0.96 < 1 → 可清算
  4. 用第三方账户调用 liquidate(user, amount) → 清算人偿还部分债务,按 5% 折扣拿走抵押品 → 观察清算人获利、被清算人损失。
  5. 计算"安全边界":给定 LTV 与阈值,价格跌多少会被清算?做一张对照表指导实际用仓。

验证命令

cast call <lending> "healthFactor(address)(uint256)" <user> --rpc-url http://127.0.0.1:8545
# 期望:价格 1200 时返回值 < 1e18

完成标准:给定抵押率与币价,能口算出清算价位。

常见错误与排查:HF 计算溢出 → 用 1e18 精度并注意乘除顺序;清算不触发 → 检查是否漏了"价格更新"这一步。

安全提示:真实市场中价格下跌往往伴随网络拥堵与 gas 飙升,"跌到清算线再补仓"通常来不及。级联清算会进一步压低价格,形成死亡螺旋(参见实验 38 的事故复盘)。

重置方法:重启 anvil。

进阶挑战:加入"利息累积",模拟长期持仓在无价格变动下也会因利息而 HF 下降。

思考题:清算奖励是给清算人的补偿,也是被清算人的额外损失。这个数字定多高才合理?

对应章节:第 18 章 借贷与杠杆


实验27:稳定币机制模拟器

等级:进阶 适合角色:所有人 / 分析师 风险等级:无风险(本地教学合约) 预计时间:90 分钟 前置知识:实验 24、26 学习目标:分别实现三类稳定币(法币抵押、超额抵押、算法)的最小模型,理解各自的锚定机制与失效条件。

场景:三种稳定币在正常时期看起来一样,都是 1 美元。差别只在压力测试时暴露。

环境要求:Foundry + anvil 或纯 JS 模拟。

初始化命令

cd ~/web3-labs/dev && anvil

操作步骤 / 每步预期结果

  1. 法币抵押型:合约维护 reserve 变量,mint 需 owner 确认已收到 1 美元。压力测试:把 reserve 改成 0.8 → 代币仍能流通,但赎回会失败 → 风险点在链下,链上完全看不出来
  2. 超额抵押型(DAI 式):复用实验 26 的借贷逻辑,抵押 150% 铸造稳定币。压力测试:抵押品暴跌 40% → 触发清算,若清算不及时则产生坏账 → 全系统资不抵债。
  3. 算法型(双代币):实现"稳定币 < 1 时可换 1 美元等值的治理币"的套利机制。
// 教学模型:脱锚时的死亡螺旋
let stable = 1e9, gov = 1e8, govPrice = 10;
function arb(burnStable) {                 // 销毁稳定币铸治理币
  const mintGov = burnStable / govPrice;
  stable -= burnStable; gov += mintGov;
  govPrice = govPrice * (1 - mintGov / gov);  // 增发压低币价
  return govPrice;
}
  1. 运行螺旋:连续赎回 → 治理币价格加速下跌 → 每次赎回需铸更多治理币 → 数轮后 govPrice 趋近 0 → 锚定彻底失效
  2. 画出三种模型在"抵押品跌 50%"下的存活曲线。
  3. 总结每类的关键监控指标:法币型看审计报告与储备构成;超额型看抵押率与清算延迟;算法型看治理币市值/稳定币市值比。

验证命令

node algo-stable.js    # 期望看到 govPrice 逐轮下降至接近 0

完成标准:能针对任一稳定币,说出它的锚定来源与三个失效场景。

常见错误与排查:模型中忘记"治理币市值必须远大于稳定币市值"这个前提 → 那正是算法稳定币的核心脆弱点。

安全提示:本实验是简化教学模型,不构成对任何具体稳定币的评价或投资建议。历史上多个算法稳定币在数日内归零,评估时请关注"极端行情下谁来兜底"这一问题。

重置方法:重启 anvil / 删除脚本目录。

进阶挑战:给算法模型加入"储备基金",测算需要多大规模的储备才能扛住 30% 的赎回冲击。

思考题:稳定币的"稳定"来自机制还是来自信心?两者能被拆开吗?

对应章节:第 19 章 稳定币


实验28:Oracle 操纵模拟(本地教学合约)

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

安全边界(务必先读):本实验只在本地链上、对你自己部署的、故意写成脆弱版本的教学合约进行。 目的是理解为什么"用现货池价格做预言机"是危险的,以及如何修复本实验不提供、也禁止推导任何针对真实协议的攻击代码、攻击交易或资金路径。 所有教学合约源码首行必须标注:// 仅本地教学,请勿部署到主网

学习目标:亲手看到"单一现货池价格"如何在一笔交易内被推动,并掌握 TWAP、多源聚合、偏离熔断三种修复手段。

场景:一个借贷协议用某个流动性很浅的 DEX 池子作为价格来源。

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

初始化命令

cd ~/web3-labs/dev && anvil

操作步骤 / 每步预期结果

  1. 部署脆弱版预言机(教学):
// 仅本地教学,请勿部署到主网
function getPrice() external view returns (uint256) {
    (uint256 rA, uint256 rB) = amm.getReserves();
    return rB * 1e18 / rA;          // 直接读现货储备 —— 这就是漏洞
}
  1. 在浅池(1000/1000)中做一笔大额兑换 → getPrice() 立刻从 1.0 变成 4.0 左右。
  2. 观察借贷合约的 healthFactor 随之剧变 → 理解"价格失真 → 风控失效"的传导链条。
  3. 恢复池子,价格回落 → 证明这种失真是瞬时且可在单笔交易内构造的
  4. 修复练习一(TWAP):改为记录累计价格,取 30 分钟时间加权均值 → 重跑步骤 2,价格几乎不动,因为瞬时冲击被时间平滑。
  5. 修复练习二(多源 + 熔断):接入两个独立价格源,若偏离超过 2% 则 revert 并暂停借贷 → 重跑步骤 2,交易直接失败。
  6. 写下审计要点:预言机来源是什么?是否单源?是否用现货?池子深度多少?有无偏离检查与更新延迟上限(staleness check)?

验证命令

cast call <oracle> "getPrice()(uint256)" --rpc-url http://127.0.0.1:8545      # 脆弱版:兑换后剧变
cast call <twapOracle> "getPrice()(uint256)" --rpc-url http://127.0.0.1:8545  # 修复版:基本不变

完成标准:能在代码评审中一眼指出"直接读 getReserves 做定价"的风险,并给出至少两种修复方案。

常见错误与排查:TWAP 窗口太短(如 1 分钟)仍可被操纵 → 窗口长度需与攻击成本一起权衡;多源都来自同一底层数据 → 不是真的多源。

安全提示:TWAP 也不是银弹——在流动性极低的池子上,即使 TWAP 也可能被跨块持续推动。最稳妥的做法是:低流动性资产不作为抵押品。

重置方法:重启 anvil,所有教学合约清除。不要将本实验的任何合约部署到测试网或主网。

进阶挑战:给借贷合约加上"抵押品必须满足最低流动性阈值"的准入规则,从产品层面消除这类风险。

思考题:预言机把链下现实带上链,也把链下的脆弱性带上链。"去信任"系统里的这个信任点,能被彻底消除吗?

对应章节:第 20 章 预言机


实验29:DAO 治理模拟

等级:进阶 适合角色:所有人 / 治理参与者 风险等级:无风险(本地链) 预计时间:90 分钟 前置知识:实验 19、23 学习目标:跑通"提案 → 投票 → 时间锁 → 执行"的完整治理流程,并亲手演示"代币投票 = 财富投票"的结构性问题。

场景:一个协议宣称由社区治理。但持币前 5 名地址合计占 60% 投票权。

环境要求:Foundry + OpenZeppelin Governor + anvil。

初始化命令

cd ~/web3-labs/dev && forge install OpenZeppelin/openzeppelin-contracts --no-git && anvil

操作步骤 / 每步预期结果

  1. 部署 ERC20Votes 治理代币 + Governor + TimelockController
  2. 把代币分给 5 个账户(比例 50/20/15/10/5),各自 delegate 给自己 → 未 delegate 的代币没有投票权,这是新手最常见的困惑。
  3. 发起提案:把某个参数从 100 改成 200 → 返回 proposalId,状态为 Pending。
  4. anvil 挖若干块跨过投票延迟 → 状态变 Active。
  5. 账户 0(50%)投赞成 → 提案达到法定人数并通过,其余四人合计 50% 反对也无法阻止 → 直观看到财富即权力
  6. 排队进入 Timelock → 等待延迟 → execute → 参数生效。
  7. 变体实验:把法定人数(quorum)从 4% 提到 40% → 提案因参与度不足而失败 → 理解"高门槛防攻击,也导致治理瘫痪"的两难。

验证命令

cast call <governor> "state(uint256)(uint8)" <proposalId> --rpc-url http://127.0.0.1:8545
# 0=Pending 1=Active 3=Defeated 4=Succeeded 5=Queued 7=Executed

完成标准:能完整跑一次治理流程,并说出三种治理攻击面(借票攻击、低参与度、提案伪装)。

常见错误与排查:投票权为 0 → 忘了 delegate,或 delegate 发生在快照区块之后;提案 execute 失败 → Timelock 延迟未到。

安全提示:治理提案的 calldata 可以伪装。投票前必须解码提案实际调用的函数与参数(复用基础卷实验 13 的技能),历史上有提案表面是"调整参数"实际是"转移资金库"。

重置方法:重启 anvil。

进阶挑战:模拟"闪电贷借票"——在同一区块借入大量代币投票(本地教学合约),然后实现快照机制(基于历史区块的余额)来防御它。

思考题:一人一票在链上无法实现(女巫攻击),一币一票导致寡头。有没有第三条路?

对应章节:第 16 章 协议治理与权限


实验30:Rollup 批处理模拟

等级:进阶 适合角色:开发者 / 架构好奇者 风险等级:无风险(本地链模拟) 预计时间:80 分钟 前置知识:实验 18、22 学习目标:用两条本地链模拟 L1/L2 关系,理解"批量提交 + 数据可用性 + 争议期"这三件事如何共同构成 Rollup 的安全模型。

场景:以太坊每秒只能处理十几笔交易。Rollup 的思路是"在别处算,把结果和数据压缩后交回主链"。

环境要求:两个 anvil 实例(模拟 L1 与 L2)+ Node.js。

初始化命令

anvil --port 8545 --chain-id 1337   # 扮演 L1
anvil --port 8546 --chain-id 1338   # 扮演 L2(另一个终端)

操作步骤 / 每步预期结果

  1. 在 L2 上执行 50 笔转账 → 记录每笔的 from/to/amount 和最终状态根。
  2. 写"批处理器"脚本:把 50 笔交易压缩成一个 calldata blob + 一个状态根哈希。
  3. 在 L1 上部署 Inbox 合约(教学版),把 blob 与状态根作为一笔交易提交 → 观察 L1 只花了一笔交易的 gas 承载了 50 笔的数据。
  4. 计算压缩比:50 笔独立 L1 交易 gas ≈ 50 × 21000;批量提交 ≈ 数万 gas → 得出成本降低倍数。
  5. 数据可用性实验:把 blob 内容删掉只留状态根 → 尝试从 L1 数据重建 L2 状态 → 失败。理解"没有 DA 的方案(Validium)在排序器作恶时用户无法自证资产"。
  6. 争议期实验:提交一个错误的状态根,实现一个简化的 challenge(proof) 函数,在 7 天窗口内任何人可挑战并回滚 → 理解 Optimistic Rollup 的提款延迟从哪来。
  7. 对比 ZK 路线:把"挑战"换成"提交有效性证明即刻终局"(本实验用一个 mock verifier 占位)。

验证命令

cast receipt <批量提交hash> --rpc-url http://127.0.0.1:8545 | grep gasUsed
# 与 50 × 21000 = 1,050,000 对比

完成标准:能说清 Optimistic 与 ZK Rollup 在"终局性、成本、信任假设"上的三点差异。

常见错误与排查:两个 anvil 端口冲突 → 显式指定 --port;状态根对不上 → 序列化顺序不一致。

安全提示:多数 Rollup 目前仍有中心化排序器与升级密钥。"L2 的安全性继承自 L1"这句话有前提条件,评估时应查看该 L2 是否已开放无许可提款与欺诈证明

重置方法:分别重启两个 anvil。

进阶挑战:实现一个"强制包含"(force inclusion)通道,让用户在排序器审查交易时仍能通过 L1 提交。

思考题:如果排序器可以任意排序交易,L2 上的 MEV 归谁?用户能做什么?

对应章节:第 21 章 扩容与 Layer 2


实验31:跨链桥信任模型

等级:进阶 适合角色:所有人 / 安全研究者 风险等级:无风险(本地双链模拟) 预计时间:90 分钟 前置知识:实验 30 学习目标:用双本地链实现一个最小"锁定—铸造"桥,逐层剥出它的信任假设,理解为什么跨链桥是历史上损失最惨重的一类基础设施。

场景:你把资产从链 A 转到链 B。链 B 上凭什么相信"你真的在链 A 上锁了资产"?

环境要求:两个 anvil(同实验 30)+ Node.js relayer 脚本。

初始化命令

anvil --port 8545 --chain-id 1337
anvil --port 8546 --chain-id 1338

操作步骤 / 每步预期结果

  1. 链 A 部署 Vault(锁定并 emit Locked(user, amount, nonce));链 B 部署 WrappedTokenonlyRelayer mint)。均标注 // 仅本地教学,请勿部署到主网
  2. 写 relayer 脚本:监听链 A 的 Locked 事件 → 在链 B 调用 mint
  3. 完整跑通一次跨链 → 链 A 余额减少,链 B 出现等量 wrapped 代币。
  4. 信任点暴露实验(本地,仅演示逻辑缺陷): - 把 relayer 私钥想象成泄露 → relayer 可以在链 B 无中生有地 mint。这说明桥的安全上限 = relayer 密钥的安全上限。 - 去掉 nonce 去重 → 同一事件被 relay 两次,链 B 多铸一份。这就是"重放"类缺陷的形态。 - 不校验事件来源合约地址 → 任何合约 emit 同名事件都会被采信。这是"事件伪造"类缺陷的形态。
  5. 逐一修复:nonce 去重表、事件来源地址白名单、relayer 改为 m-of-n 多签、加入金额上限与速率限制。
  6. 画出三种桥模型的信任图:外部验证(多签/MPC)、乐观验证(挑战期)、原生/轻客户端验证(链上验证对方共识),标出各自的信任假设与成本。

验证命令

cast call <wrapped> "totalSupply()(uint256)" --rpc-url http://127.0.0.1:8546
# 期望:等于链 A 上 Vault 的锁定总量,任何不等都意味着桥失衡

完成标准:能对任意一座桥回答三个问题——谁在做验证?验证失败会怎样?我需要信任几个实体?

常见错误与排查:relayer 漏事件 → 需实现从上次处理高度续传;跨链 nonce 冲突 → 用 (srcChainId, nonce) 组合键。

安全边界(重要):本实验的所有缺陷演示仅在本地自建教学桥上进行,用于理解"应该校验什么"。不涉及任何真实桥、不提供任何针对真实桥的攻击路径。

安全提示:使用跨链桥时优先选择"锁定量可在链上验证、有金额上限与暂停机制、经过多次审计且长期运行"的方案。跨链持仓不宜过夜留大额。

重置方法:重启两个 anvil。

进阶挑战:实现 relayer 的 2-of-3 多签,验证单个 relayer 私钥泄露不足以铸币。

思考题:如果桥的本质是"跨信任域传递消息",那么"绝对安全的桥"是否在理论上就不存在?

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


实验32:零知识证明直觉实验

等级:进阶 适合角色:所有人 / 开发者 风险等级:无风险(本地计算) 预计时间:80 分钟 前置知识:基础卷实验 4、5 学习目标:不碰复杂数学,通过三个动手环节建立"完备性、可靠性、零知识性"的直觉,并理解 ZK 在扩容与隐私中的两种不同用法。

场景:你要证明"我知道某个密码",但不能透露密码本身;或者证明"我算对了一百万笔交易",但验证者不想重算一百万次。

环境要求:Node.js;可选 Python 画图。

初始化命令

mkdir -p ~/web3-labs/dev/zk && cd ~/web3-labs/dev/zk && npm init -y

操作步骤 / 每步预期结果

  1. 环节一:藏宝图(零知识性直觉)。用哈希承诺实现"我知道 x 使得 H(x)=y":先公布 y,再用挑战-响应方式证明,全程不公布 x。
const { createHash } = require('crypto');
const H = s => createHash('sha256').update(String(s)).digest('hex');
const secret = 42, commitment = H(secret);        // 公开 commitment
// 验证者只能通过 commitment 检查,无法反推 secret
console.log('承诺:', commitment);
  1. 环节二:Schnorr 式交互证明(可靠性直觉)。实现"证明知道离散对数"的三步协议(承诺 → 挑战 → 响应),并演示:如果证明者不知道秘密,只能猜对挑战,成功率 1/2 → 重复 40 轮后作弊概率低于 1e-12。
  2. 运行 40 轮 → 输出"作弊者成功概率 ≈ 9.09e-13",理解"概率性证明为何足够可靠"。
  3. 环节三:验证成本(扩容用法)。写一个函数计算 100 万次哈希(耗时数秒),再写一个"证明"只是最终结果 + 一个固定长度摘要 → 对比"重算"与"验证"的时间差 → 这就是 ZK Rollup 省的东西。
  4. 区分两种用途:隐私(不泄露输入,如隐私转账)与简洁性(验证比重算快,如 ZK Rollup)。注意大多数 ZK Rollup 用的是后者,并不提供隐私
  5. 了解可信设置(trusted setup)的存在及其风险:若初始参数的"废料"未被销毁,可伪造证明。

验证命令

node schnorr-sim.js   # 期望输出 40 轮后作弊概率 < 1e-12

完成标准:能用生活化比喻向非技术者解释 ZK 的三个性质,并说清 ZK Rollup 为什么通常不等于匿名。

常见错误与排查:把 ZK 理解成"加密" → ZK 是"证明",不是加密;以为所有 ZK 都需要可信设置 → STARK 类不需要。

安全提示:ZK 电路的 bug 极难发现且后果严重(可无限铸币)。评估 ZK 项目时应关注电路是否开源、是否被独立审计、可信设置的参与者规模。

重置方法:删除目录。

进阶挑战:用现成的 ZK 工具链(如 circom/snarkjs 的本地示例)跑通一个"证明我知道某数的平方根"的完整电路。

思考题:如果验证一个证明只要几毫秒,而生成它要几分钟,这种不对称性还能催生哪些应用?

对应章节:第 23 章 零知识与隐私


实验33:链上交易追踪

等级:进阶 适合角色:分析师 / 安全研究者 / 记者 风险等级:无风险(只读公开数据) 预计时间:90 分钟 前置知识:实验 22 学习目标:掌握一笔交易的"解剖"方法——内部调用、代币流向、多跳资金路径,并建立地址聚类的基本启发式。

安全边界:本实验只做只读查询公开链上数据,不发起任何交易、不涉及任何私钥、不针对特定个人做人肉搜索。分析对象应为公开的协议地址或已被广泛报道的历史事件地址。

场景:你看到一笔可疑的大额转账,需要搞清楚钱从哪来、到哪去、中间经过了什么合约。

环境要求:Node.js + viem;本地链(练手)+ 可选公共只读 RPC。

初始化命令

mkdir -p ~/web3-labs/dev/trace && cd ~/web3-labs/dev/trace && npm init -y && npm i viem

操作步骤 / 每步预期结果

  1. 先在本地链造数据:A → 合约 B → 合约 C → D 的多跳转账,制造一条可追踪路径。
  2. debug_traceTransaction(anvil 支持)拿到内部调用树:
cast rpc debug_traceTransaction <hash> '{"tracer":"callTracer"}' --rpc-url http://127.0.0.1:8545
# 做什么:输出完整调用树,包括外部交易看不到的 internal calls
# 正常输出:嵌套的 {type, from, to, value, input, calls[]}
# 失败排查:method not found → 该 RPC 未开 debug 命名空间,改用日志推断
  1. Transfer 事件重建代币流向图(复用实验 22 的 getLogs)→ 输出 "from → to : amount" 序列。
  2. 写脚本做 N 跳追踪:从起点地址出发,逐跳跟随最大金额的流出 → 生成资金路径。
  3. 建立聚类启发式并明确其局限:共同花费、相同资金源、时间聚集、gas 价格指纹 → 这些都是概率性线索,不是身份证明
  4. 应用到只读的公开历史案例:选一个已被公开报道的事件地址,复现"资金流向混币器/交易所"的路径(只看不动,不做任何交互)。

验证命令

node trace.mjs <txhash>   # 期望输出调用树深度与代币流向表

完成标准:给定一笔交易 hash,能产出"调用树 + 代币流向 + 资金去向"三张表。

常见错误与排查:只看 to 字段以为交易只涉及一个合约 → 必须看内部调用;把合约地址当成"个人" → 区分 EOA 与合约。

安全提示:链上分析的结论是概率性的。把地址与真实身份关联需要极高的证据标准,错误指认会造成严重伤害。分析结果应表述为"该地址与 X 存在资金关联",而非"该地址属于某人"。

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

进阶挑战:把追踪结果输出为 DOT 格式,用 graphviz 渲染成资金流向图。

思考题:链上公开透明常被称为优点。对普通用户而言,"所有人都能看到我的全部交易历史"是优点还是风险?

对应章节:第 7 章 链上数据与可验证性


实验34:协议指标分析

等级:进阶 适合角色:分析师 / 投资者 / 产品 风险等级:无风险(只读数据) 预计时间:80 分钟 前置知识:实验 22、33 学习目标:自己算出 TVL、交易量、活跃地址、手续费收入等核心指标,理解每个指标的口径陷阱与可被操纵的方式。

场景:某协议宣称 TVL 10 亿美元、日活 5 万。这两个数字分别可能怎么被夸大?

环境要求:Node.js + viem;本地部署的 AMM(实验 24)作为练习对象。

初始化命令

mkdir -p ~/web3-labs/dev/metrics && cd ~/web3-labs/dev/metrics && npm init -y && npm i viem

操作步骤 / 每步预期结果

  1. 在本地 AMM 上造 200 笔交易、若干 LP 存取,形成可分析的数据集。
  2. TVL:读池子里两种代币的余额 × 各自价格 → 得出总锁仓价值。
  3. 口径陷阱一(重复计算):把同一份资产在"存入 → 铸出凭证 → 再存入另一协议"的场景中重复计入 → TVL 虚增数倍。写代码复现这个虚增。
  4. 交易量:从 Swap 事件累加 → 得日交易量。
  5. 口径陷阱二(刷量):用 5 个地址互相对敲 100 笔 → 交易量翻倍但真实用户为 0 → 加上"独立地址数""新地址占比"作为交叉校验。
  6. 活跃地址:统计去重的 from 地址;再统计"首次出现地址占比"与"资金来源集中度" → 若大量新地址的初始资金来自同一地址,高度疑似女巫。
  7. 手续费收入 vs 代币激励:算出协议真实收入与发放的代币激励价值 → 若激励远大于收入,说明增长由补贴驱动,不可持续。
  8. 产出一张指标卡:TVL、日量、DAU、真实收入、激励支出、收入/激励比。

验证命令

node metrics.mjs   # 期望输出 6 项指标及"刷量嫌疑"标记

完成标准:能对任一协议给出"表面指标 + 质量调整后指标"两组数字。

常见错误与排查:TVL 用错价格源导致虚高 → 使用可靠价格并注明来源;把合约地址计入活跃用户 → 需过滤。

安全提示:本实验不构成投资建议。指标分析只能识别明显异常,无法证明项目安全或值得投资。任何"数据看起来很好"的结论都应与实验 23 的权限审查、实验 36 的白皮书核查结合。

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

进阶挑战:实现"洗盘交易检测"——找出在短时间内高频互相交易且净头寸接近 0 的地址组。

思考题:如果一个指标被广泛用于评价项目,它就一定会被优化甚至操纵。有没有难以被操纵的指标?

对应章节:第 24 章 如何评估一个项目


实验35:Tokenomics 分析

等级:进阶 适合角色:投资者 / 分析师 / 创业者 风险等级:无风险(纯建模) 预计时间:80 分钟 前置知识:实验 34 学习目标:建立代币供应与解锁模型,量化"未来抛压",理解 FDV 与流通市值的差距如何决定风险。

场景:一个代币流通市值 1 亿,FDV 20 亿。这意味着未来某天会有 19 亿的新供应进入市场。

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

初始化命令

mkdir -p ~/web3-labs/dev/tokenomics && cd ~/web3-labs/dev/tokenomics && npm init -y

操作步骤 / 每步预期结果

  1. 建立分配表:团队 20%、投资人 25%、社区 30%、生态基金 15%、流动性 10%,总量 10 亿。
  2. 建立解锁曲线:团队 1 年 cliff + 3 年线性;投资人 6 月 cliff + 2 年线性;社区按挖矿逐月释放。
function circulating(month) {
  let s = 0;
  s += month >= 12 ? Math.min((month - 12) / 36, 1) * 2e8 : 0;      // 团队
  s += month >= 6  ? Math.min((month - 6) / 24, 1) * 2.5e8 : 0;     // 投资人
  s += Math.min(month / 48, 1) * 3e8;                               // 社区
  s += 1e8;                                                         // 初始流动性
  return s;
}
for (let m = 0; m <= 48; m += 3) console.log(m, (circulating(m)/1e8).toFixed(2), '亿');
  1. 运行 → 得到 48 个月的流通量曲线,注意第 12 个月的陡增(团队 cliff 到期)
  2. 计算每月新增供应 / 当前流通量 → 得出月度稀释率;若某月稀释率 > 15%,标记为高抛压窗口。
  3. 计算"吸收所需买盘":新增供应 × 币价 = 需要的净买入额,与当前日交易量对比 → 若远大于日交易量,价格承压明显。
  4. 需求侧建模:代币有什么真实用途(gas、质押、治理、费用分成)?把"必须持有代币"的场景量化 → 若只有治理,需求基础薄弱。
  5. 产出结论卡:FDV/流通市值比、最大解锁窗口、稀释率峰值、需求来源清单。

验证命令

node unlock.js | head -20   # 期望第 12 个月出现明显跳变

完成标准:能画出任一项目的解锁曲线并标出三个高风险时间窗口。

常见错误与排查:只看"当前市值便宜"忽略 FDV → 必须两者并看;忽略质押锁仓对有效流通量的影响 → 需单独扣除。

安全提示本实验不构成任何投资建议。 Tokenomics 模型只描述供应机制,不能预测价格。任何承诺收益的项目都应高度警惕。

重置方法:删除目录。

进阶挑战:加入"质押率"变量,模拟高收益质押如何暂时锁住供应,以及收益下降时的集中解锁风险。

思考题:代币经济学在多大程度上是"经济设计",在多大程度上是"营销包装"?如何区分?

对应章节:第 24 章 如何评估一个项目


实验36:白皮书事实核查

等级:进阶 适合角色:所有人 / 投资者 / 记者 风险等级:无风险(只读核查) 预计时间:90 分钟 前置知识:实验 23、34、35 学习目标:把白皮书里的每一条声明转化为"可在链上或公开来源验证的断言",训练"不看承诺看证据"的习惯。

场景:一份 40 页的白皮书,充满图表和术语。你需要在 90 分钟内判断它有没有说谎。

环境要求:浏览器(只读)+ 实验 23/34 的脚本。

初始化命令

mkdir -p ~/web3-labs/dev/fact-check && cd ~/web3-labs/dev/fact-check
# 建立核查表模板
printf "声明,类型,可验证方式,证据,结论\n" > checklist.csv

操作步骤 / 每步预期结果

  1. 通读白皮书,把所有声明分为四类:可链上验证(合约地址、供应量、权限)、可公开验证(团队、审计、合作方)、不可验证的承诺(路线图、愿景)、营销修辞("革命性""全球领先")。
  2. 统计四类占比 → 若"不可验证 + 修辞" 超过 60%,这是一个强信号。
  3. 逐条核查可链上验证项(只读): - 合约地址是否与白皮书一致? - 总供应量是否与文档相符?(totalSupply) - owner 是谁?能否 mint?(复用实验 23) - 团队代币是否真的锁在合约里?锁仓合约代码是否可查?
  4. 核查审计声明:审计报告是否公开、审计的是哪个 commit、报告中的高危项是否已修复、审计时间是否早于最后一次合约升级。
  5. 核查合作方:所谓合作伙伴是否有对方官方渠道的对应公告(单方面宣布不算合作)。
  6. 核查团队:是否实名?过往项目结果如何?头像是否为 AI 生成/图库照片(反向图片搜索)?
  7. 填完 checklist.csv,输出结论:绿灯项 / 黄灯项 / 红灯项各几条。

验证命令

cast call <token> "totalSupply()(uint256)" --rpc-url <只读RPC>   # 与白皮书数字比对
awk -F, '{print $5}' checklist.csv | sort | uniq -c              # 统计结论分布

完成标准:产出一份可复用的核查表,且每条结论都有可点击的证据来源。

常见错误与排查:把"已通过审计"当成安全保证 → 审计有范围和时间点,且不覆盖经济模型与运营风险;被术语密度迷惑 → 看不懂时要求对方用一句话解释,说不清的通常是包装。

安全提示:核查全程只做只读查询,不连接钱包、不签任何名、不转任何测试资金"试试看"。诈骗项目常用"先小额体验"作为诱饵。本实验不构成投资建议。

重置方法:删除目录。

进阶挑战:把核查流程脚本化——输入合约地址,自动输出供应量、owner、是否可 mint、是否代理、创建时间。

思考题:如果一份白皮书所有事实都为真,能证明项目值得投资吗?事实核查的边界在哪?

对应章节:第 24 章 如何评估一个项目


实验37:合约权限调查

等级:高级 适合角色:安全研究者 / 分析师 / 尽调人员 风险等级:无风险(只读 + 本地复现) 预计时间:100 分钟 前置知识:实验 23、33 学习目标:把实验 23 的清单升级为完整调查方法论——从代理结构、角色矩阵、多签构成到升级历史,产出一份可交付的权限报告。

场景:你要为一笔大额资金决定"能不能把钱放进这个协议",需要的不是感觉,而是一份权限清单。

环境要求:Node.js + viem/cast;本地部署一套代理合约作练习;只读查询公开数据作为验证。

初始化命令

mkdir -p ~/web3-labs/dev/perm-audit && cd ~/web3-labs/dev/perm-audit && npm init -y && npm i viem

操作步骤 / 每步预期结果

  1. 本地部署一套 UUPS 代理 + 实现合约 + AccessControl 多角色 + 2/3 多签作为 admin。
  2. 代理识别:读 EIP-1967 槽位。
# implementation slot
cast storage <proxy> 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc --rpc-url http://127.0.0.1:8545
# admin slot
cast storage <proxy> 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 --rpc-url http://127.0.0.1:8545

→ 分别得到实现地址与管理员地址。

  1. 角色矩阵:枚举所有 bytes32 角色常量,用 getRoleMemberCount / getRoleMember 列出每个角色的持有者 → 输出一张"角色 × 地址 × 能做什么"的表。
  2. 多签审查:若 admin 是多签,查 getOwners()getThreshold() → 关键问题:签名者是否互相独立?阈值是否合理(1/N 等于单签)?签名者地址是否有链上活动痕迹?
  3. 升级历史:查 Upgraded(address) 事件的全部历史 → 列出每次升级的时间、发起者、新实现地址 → 频繁升级本身是风险信号。
  4. 危险函数扫描:在实现合约 ABI 中筛出所有 onlyOwner/onlyRole 且能影响资金的函数(transfer、withdraw、setFee、setOracle、pause、sweep)→ 逐个评估最坏情况。
  5. 产出报告模板:权限主体 → 能力清单 → 最坏情况损失 → 是否有 timelock → 用户可撤离窗口 → 综合结论。

验证命令

node audit.mjs <proxy地址>   # 期望输出:是否代理/实现地址/admin/角色表/升级次数

完成标准:交付一份 1 页权限报告,包含"如果 admin 私钥今天泄露,用户最多损失多少"的明确答案。

常见错误与排查:只审代理不审实现 → 逻辑全在实现里;只看当前实现忽略"可被升级成任意代码" → 可升级性本身就是最大权限。

安全边界:本实验对真实合约只做只读查询cast call / cast storage / 事件查询),绝不发起任何写交易,绝不尝试调用任何权限函数。所有权限操作练习都在本地自建合约上完成。

安全提示:可升级合约意味着"今天的代码不代表明天的代码"。审计报告审的是某个 commit,升级后即失效。评估时应把"升级权限归谁"视为最高优先级问题。

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

进阶挑战:写一个通用扫描器,输入地址后自动判断代理类型(Transparent / UUPS / Beacon / Diamond)并列出全部 facet。

思考题:"协议已放弃所有权限(renounce ownership)"是最安全的吗?它同时放弃了什么?

对应章节:第 16 章 协议治理与权限


实验38:安全事故复盘

等级:高级 适合角色:所有人 / 安全研究者 / 团队负责人 风险等级:无风险(固定历史数据 + 只读公开信息) 预计时间:120 分钟 前置知识:实验 26、28、31、37

安全边界(务必先读):本实验只使用已公开报道的历史事件资料与只读公开链上数据不复现攻击、不编写攻击代码、不推导可用于攻击的步骤。 复盘的产出是"防御清单"与"响应流程",不是"攻击手册"。 如需在本地演示某类缺陷的原理,一律使用本卷实验 28/31 中已标注 // 仅本地教学,请勿部署到主网 的教学合约。

学习目标:建立一套结构化的事故复盘方法(时间线 → 根因 → 传导 → 响应 → 教训 → 检查项),并把它转化为自己项目/自己资产的防御清单。

场景:一个协议在凌晨损失了大量资金。第二天你要向团队解释"我们会不会也这样"。

环境要求:浏览器(只读查阅公开报道)+ 实验 33 的追踪脚本 + 实验 37 的权限脚本。

初始化命令

mkdir -p ~/web3-labs/dev/postmortem && cd ~/web3-labs/dev/postmortem
printf "# 事故复盘模板\n\n## 1 事实时间线\n## 2 根本原因\n## 3 传导路径\n## 4 响应过程评价\n## 5 教训\n## 6 我们的检查项\n" > template.md

操作步骤 / 每步预期结果

  1. 选题:从公开报道中选择一个已被充分披露的历史事件(建议选择官方已发布 post-mortem 的案例,资料完整且无争议)。
  2. 建立时间线:从公开来源整理"部署时间 → 审计时间 → 异常首次出现 → 团队响应 → 公告 → 后续处置",精确到小时。每条都标注来源链接。
  3. 只读验证:用实验 33 的方法查询相关公开地址的余额变化与转账时间,验证公开报道与链上记录是否一致 → 培养"以链上数据为准"的习惯。
  4. 根因归类:把原因归入五类之一——代码缺陷(如重入、访问控制)、经济设计缺陷(如预言机、清算参数)、密钥管理失误、运营/社工失误、第三方依赖失效。
  5. 传导分析:画出"根因 → 直接后果 → 二级影响(其他协议、稳定币、清算)"的因果图。
  6. 响应评价:团队多久发现?多久暂停?公告是否及时准确?有无应急预案?用户能否自救?
  7. 转化为检查项:把每条教训写成一句可执行的检查(例如"所有外部调用后必须更新状态""预言机必须多源且带偏离熔断""管理员必须多签 + timelock")。
  8. 用这份检查项对照实验 37 中你审过的合约 → 找出至少 3 条不满足项。

验证命令

wc -l postmortem-*.md    # 完成的复盘报告应包含全部 6 个章节
cast balance <公开事件地址> --rpc-url <只读RPC>   # 只读验证,不发起任何交易

完成标准:产出一份含时间线、根因、传导图、响应评价、可执行检查项的完整报告,且所有事实都有公开来源。

常见错误与排查:把"攻击手法"当复盘重点 → 复盘的价值在防御与响应,不在手法;只归因于"黑客太厉害" → 几乎所有事故的根因都是可预见的设计或流程问题;使用未经证实的传言 → 每条事实必须可溯源。

安全提示:复盘涉及的地址与个人信息应谨慎处理,不要在报告中做未经证实的身份指认(呼应实验 33 的提示)。安全研究应遵循负责任披露:发现真实漏洞时应先私下联系项目方,而非公开或利用。

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

进阶挑战:对同一类根因(如预言机)横向复盘三个不同事件,找出共同的结构性原因,产出一份"该类风险通用检查清单"。

思考题:几乎每次事故后都有人说"这个漏洞很明显"。为什么它在事发前没被发现?这说明审计与测试的边界在哪?

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


本卷小结

主题 实验
合约开发基础 18、19、20、21、22
权限与治理 23、29、37
DeFi 机制模拟 24、25、26、27、28
基础设施 30、31、32
分析与尽调 33、34、35、36
事故复盘 38

本卷贯穿的三条铁律

  1. 所有漏洞与操纵类实验只在本地链 + 自建教学合约上进行,教学合约必须标注"仅本地教学,请勿部署到主网"。
  2. 涉及真实链的一切操作只读、不写、不连钱包
  3. 实验的产出永远是识别方法与修复方案,不是攻击能力。

下一卷(labs-security.md,实验 39–53)进入 15 个专项安全实验。