T1 · EVM Engineer
从「合约能跑」走到「我知道它在什么条件下会坏,坏成什么样」。
面向以 EVM 为主战场的合约工程师。
本页里的「T1」指这条 Track。课程章节一律写成「第 T8 章」并带链接,两者不是一回事——章节 T1 讲的是链上数据究竟是什么。
这条 Track 适合谁
不看岗位名,看你现在卡在哪。下面四种状态,命中一条就适合进来。
你写得出合约,但说不清它为什么安全。 部署过测试网,测试全绿,覆盖率好看。有人问「你怎么知道它是安全的」,你能给出的最强回答是「测试都过了」。你自己也知道这句话不够,但不知道还能补什么。
你读不懂主网上的真实代码。 打开一份线上协议的核心合约,里面是代理、内联汇编、位压缩、自定义错误。你分不清哪些是必要的工程取舍,哪些只是作者在炫技,于是也不敢抄。
你的 Gas 优化靠背口诀。 记得「用 calldata 比 memory 便宜」「变量要打包」,但给你一份合约,你算不出它的 storage layout,也说不出把哪两个变量换个位置能省多少。
你在面试里答得出定义,答不出后果。 「delegatecall 是在调用方的存储上下文里执行被调方的代码」——这句话你会背。再追问「所以它会怎么出事」,你只能说「会有存储冲突」,举不出一个具体的例子。
这条 Track 的分界线只有一句话:你交付的合约,附带的是「我测过了」,还是「我知道它在什么条件下会坏,以及那时候会坏成什么样」。
如果你还没在任何测试网上部署过东西,先别进来。回去把 Part B 第二阶段做完,这条 Track 默认你已经有一个能跑的仓库。
前置
Part B 全 36 章读完当然最好。但如果你想现在就开始,知识图谱里通向这条 Track 的最短路径是这一串:
- 第 6 章
- 第 T1–T4 章
- 第 T5 章
- 第 9 章
- 第 T7 章
- 第 T8 章
- 第 T9 章
- 第 T12 章
- 第 T28 章
| 必须先读 | 图谱里的理由 |
|---|---|
| 第 6 章 · 一次链上交易发生了什么 | 第 T1 章的直接前置。不先把「一次交易」当成一段状态变更来理解,后面所有关于顺序的讨论都接不上 |
| 第 T1 章 到 第 T4 章 | 状态、签名、交易生命周期、RPC。这四章是 Part B 里唯一没有捷径的部分 |
| 第 T5 章 · EVM Account Model | 第 T7 章的直接前置。Storage slot 是这条 Track 从头到尾的计价单位 |
| 第 9 章 · Smart Contract 改变了什么 | 图谱里它同样是第 T7 章的直接前置,也是 Part A 那一侧唯一的硬依赖 |
| 第 T7 章、第 T8 章、第 T9 章 | 语言、EVM 成本模型、标准。这条 Track 的正题从这里开始 |
| 第 T12 章 · Testing & Deployment | 图谱里它依赖第 T7、T9 章。Fork 测试是这条 Track 的日常,不是某一章的练习 |
| 第 T28 章 · Smart Contract Security | 图谱里它依赖第 T8、T9、T12 章。三条都补齐了,你才读得懂它为什么强调顺序而不是语法 |
你会学什么
- Solidity
- Foundry
- Hardhat
- EVM Internals
- DeFi
- Security
六个 topic 不是六门课,是四组问题。
一、语言与工具链:把「能编译」换成「能被验证」
Solidity 的深水区从继承的线性化顺序开始,一路到自定义错误的 revert 数据、unchecked 的正确使用范围、以及短暂存储适合放什么。这些都不是语法题,而是「这一行会让谁付多少钱」的问题。
两套主流工具链不是二选一。一套以合约语言写测试、把 Fuzzing 和不变量当成一等公民;另一套以脚本语言做部署、任务编排和前端集成。现实里成熟仓库常常两套都在跑:测试与 Fuzzing 在前者,部署与集成在后者。这条 Track 要求你两套都能从空目录搭起来,并说得清这个仓库为什么这样分工。
二、EVM Internals:把 Gas 从玄学变成算术
要能不部署就写出一份合约的 storage layout:槽位怎么分配、什么条件下会被打包进同一个槽、mapping 与动态数组的槽位怎么算。在此基础上,calldata、memory、storage 的成本差就不再需要背,而是能算。
然后是代理。delegatecall 改写的是「这是谁的存储」,几种主流代理布局的差别,本质上都是在回答「怎样让实现合约的变量不踩到代理自己的变量」。
自检方式很具体:给你一份别人的合约,你能指出哪两个变量应该换位置,以及换完之后省下的是一次冷写还是一次热写。
三、DeFi:你的合约要和谁组合
不需要做到 Track T4 的深度,但要能读懂主流协议的核心合约,并且知道它们会怎样和你的代码互相伤害:
- 不返回 bool 的转账、带税代币、余额会自己变的代币、小数位不是 18 的代币
- 精度与取整——取整方向必须永远对协议有利,这条在第 T28 章讲过
- 预言机接口的失效模式:数据过期、偏离超阈值、返回零
- 带回调的代币标准会把重入带进「我没转过 ETH」的函数里
四、Security:把安全做成会自动运行的东西
第 T30 章的威胁模型四问和不变量三类,在这条 Track 里从「读过」变成「每个项目都写一遍」。配套的是有状态 Fuzzing、当成过滤器而不是判决书的静态分析、以及权限设计:多签、时间锁、暂停开关各自防的是什么。
毕业作品
一套经过 Fuzzing 与 Fork 测试的主网合约。
和毕业项目的区别要先说清楚:毕业项目是 Core 的收尾,要求横向完整——合约、前端、后端、索引、部署一条龙。这条 Track 的作品只要合约这一层,但纵向要深得多。你可以拿毕业项目的合约层当起点,但按下面五条重新验收一遍。
别人能不能跑起来。 clone 之后,一条命令装依赖,一条命令跑完全部测试。README 写明需要哪些环境变量、Fork 用的 RPC 从哪来、以及在没有归档节点时怎么退化运行。
跑不起来的仓库在招聘和合作里等于不存在,这一条永远排第一。
Fuzzing 真的在跑,而且真的红过。 至少五条不变量,三类各有覆盖。仓库里要留下一次「故意破坏一条、它被抓到」的记录:那条最短失败序列,连同当时的随机种子。
一个永远绿的检查和一个不存在的检查效果相同,但前者会给你虚假的安心。
每一个外部依赖都有 Fork 测试。 凡是你的合约会调用的外部地址,都要有一条在 Fork 上跑的用例。写清楚你 Fork 的是哪条链、哪个高度,以及为什么选这个高度。
Gas 有基线,涨了能解释。 关键函数的 Gas 快照进版本库。任何一次改动让某个数字变大,你要能说出多出来的是哪一次存储写入或哪一次外部调用。
有威胁模型和升级路径。 四问全部回答,第四问「你明确不防什么」至少写两条。如果合约可升级,写清楚谁能升级、多久生效、以及怎么停。
主网部署这一步:用你自己的钱,小额,而且只部署已经跑完前四条的版本。 只交测试网部署加一份「为什么现在还不该上主网」的说明,在评审里不扣分——那反而说明你有判断力。
怎样判断自己达标了
第一种是存储冲突。代理和实现合约各有自己的变量布局,实现合约写它的第 0 个槽时,踩的是代理的第 0 个槽。如果代理把实现地址存在那里,一次普通的业务赋值就能把合约指向任意代码。
第二种是身份保留。被 delegatecall 的代码里,msg.sender 和 msg.value 还是最外层调用者的值。被调合约如果自己有权限检查,它会以为自己被直接调用了,检查因此失效。
加分项:被 delegatecall 的那个地址如果可以被销毁或替换,代理会变成一个指向空代码的壳——调用不 revert,只是什么都不做。
把三次读收进一个局部变量:第一次是冷读,后两次在同一笔交易里本来就是热读,所以省下的是两次热读,不是两次冷读——这个数字比多数人以为的小。
真正大头通常在写:能不能把多次写合并成一次,或者把几个小变量打包进同一个槽让它变成一次写。
回答里必须出现「冷读和热读的差价」和「用快照前后对比」。 只说「用 memory 更便宜」是背口诀,不是算账。
转账不返回 bool,你按标准写的调用会因为解码失败而 revert;转账带税,收到的数量少于你传进去的数量;余额会随时间自己变化,你存下来的份额对不上;小数位不是 18,所有比例计算都错了一个量级;转账带回调,把重入带进你以为没有外部调用的函数;地址有黑名单,正常转账也可能 revert。
正确做法是同一条:以「自身余额在转账前后的差」为准,既不信返回值,也不信你传进去的参数。
不该拿审计报告当答案。该给的是三样东西:不变量清单加上它们的 Fuzzing 运行记录;威胁模型第四问里那几条「我明确不防什么」;以及关键路径在 Fork 上的测试。
前两样说明你知道边界在哪,第三样说明你验证过边界之内。审计报告只是第四样,而且它是一次快照。
没有标准答案,检查三件事:
- 你说的是一条路径(谁、用多少钱、按什么顺序调用),还是只报了一个漏洞名词。
- 你能不能说出触发条件——需要什么样的市场状态或权限状态。
- 你能不能说出「从它发生到你知道」要多久。答不出这一条,说明你还没有监控。
常见误区
「覆盖率 100% 说明测够了」
覆盖率衡量的是每一行被执行过,不是每一种调用顺序被执行过。真实漏洞几乎都藏在序列里:几个函数单独看都对,特定顺序下舍入累积或状态错位就出事。
正确做法:把覆盖率当下限门槛,信心来自有状态 Fuzzing 和不变量。
「小类型更省 Gas」
把 uint256 换成 uint8 不一定省钱。只有当几个小变量真的被打包进同一个槽时才省;没打包成功的时候,编译器为了截断和掩码反而要多做运算,比原来更贵。
正确做法:先量,再改,再量。优化前后各留一份快照,省下来的数字要能对应到具体的槽位变化。
「可升级 = 更安全」
可升级只是把风险换了个位置:从「代码可能写错」换成「升级权限可能被滥用或被盗」。后者的爆炸半径大得多,而且一次到位——一次恶意升级可以让所有不变量同时失效。
正确做法:先问这个合约到底需不需要可升级。确实需要的,把升级权限本身当成协议里最高危的资产来设计:多签、时间锁、以及一个公开可查的升级流程。
「审计过了就安全」
审计的范围就是它的能力边界,而范围是你和审计方一起定的。你没提供威胁模型,范围默认只剩「代码实现的正确性」,经济假设整个不在里面。第 T30 章那个「17 个发现全修完仍被搬空」的例子讲的就是这件事。
正确做法:审计之前先交威胁模型,审计之后把每一条发现转成一条不变量。前者决定他们查什么,后者决定这次审计的成果会不会随着下一次提交流失。
接下来
- 把这三章重读一遍,这次带着自己的仓库读:第 T8 章、第 T12 章、第 T30 章。
- 常见组合:加 Track T4 · DeFi Engineer 补金融逻辑,加 Track T5 · Security Engineer 把安全做成主业,加 Track T3 · Onchain Data Engineer 让你的协议自带可信数据。
- 跨到非技术侧:Track N2 · Crypto Product。合约工程师最常被问的不是「这段代码怎么写」,而是「为什么这个操作要签两次名」——答得好的人,话语权完全不一样。
- 想把合约层扩成完整系统,去毕业项目。
- EVM 的成本模型和操作码会随协议升级变化,怎样跟上给了订阅这些变化的方法。