审计不是找危险关键词
EVM 审计的本质,是追踪状态和价值如何经过一连串调用发生变化。

看到任何函数时,不要先搜索 call 或 delegatecall。先沿着执行路径回答下面七个问题:
学完本课,你应该能从原始 calldata 识别函数调用,解释代理的工作方式,并从 trace 还原一次多合约调用。
先建立“世界计算机”的画面
文字适合精确,视频适合建立空间感。看完后回到上面七个问题,检查自己能不能用自己的话重讲一遍。
账户与交易:谁让状态机开始运行
以太坊有两类账户。EOA 由私钥控制,可以主动发起交易;合约账户由代码控制,只会在收到调用时执行。
外部拥有账户
- 由私钥签名
- 拥有 nonce 和余额
- 可以发起一笔交易
- 没有合约代码
合约账户
- 由部署代码定义行为
- 拥有 storage 和余额
- 只能被交易或合约调用触发
- 不能自行在未来醒来
一笔交易至少要读懂这些字段
from签名者恢复出的 EOA 地址,也就是交易的最初发起者。
to入口地址。它可能是普通合约,也可能只是一个代理。
value随交易发送的原生 ETH 数量,不等于 ERC-20 转账金额。
inputcalldata。通常由 4 字节函数选择器和 ABI 编码参数组成。
receipt执行后的状态、Gas 使用量、事件日志和合约创建地址。
所谓“内部交易”不是区块中的另一笔真实交易,而是执行 trace 中合约之间的内部调用。事件日志也不是状态本身。
四种数据位置,不同的生命周期
大量 Solidity 安全问题,都与“数据究竟放在哪里”有关。特别是可升级代理,storage 布局一旦错位,代码看似正确也会读写错误变量。
contract Vault {
uint256 public totalAssets; // storage
function deposit(uint256 amount, bytes calldata proof) external {
uint256 fee = amount / 100;
totalAssets += amount - fee; // write storage
}
}ABI 与 calldata:从十六进制恢复意图
函数选择器是规范化函数签名 Keccak-256 哈希的前 4 字节。例如 transfer(address,uint256) 的选择器是 0xa9059cbb。
静态类型通常直接放在 32 字节槽位中。动态类型会先放偏移量,再在后面存放长度与内容。
事件可以与状态变化不一致。恢复调用意图要从 calldata 和代码路径开始,再用事件与余额变化相互验证。
call、delegatecall 与 staticcall
三者最大的差别不是语法,而是代码在哪个上下文执行、谁的 storage 被写入,以及是否允许修改状态。

call目标合约目标合约当前调用合约delegatecall目标合约调用方合约保留上层调用者staticcall目标合约禁止写入当前调用合约代理不是把自己的状态复制给实现。它是在代理上下文中运行实现代码,因此新逻辑可以继续读写原来的代理 storage。
失败与回滚:理解调用边界
当调用帧以 REVERT、异常或耗尽 Gas 失败时,该调用帧及其子调用造成的状态变化会被撤销。错误如果继续向上传播,整笔交易都会回到执行前状态。
但 Gas 已经被消耗,交易和失败结果仍会被区块记录。使用低级 call 时,上层可以读取返回的 success 并选择继续执行。
(bool success, bytes memory data) = target.call(payload);
if (!success) {
failedAttempts[msg.sender] += 1;
}Lab:编码、读取、解码
目标不是记住命令,而是亲眼验证函数签名如何变成 calldata。下面可以先在网页里跑一遍,再去自己的终端复现。
保存一段真实 calldata,并逐字段标注 selector、参数类型与解码值。可以截图本页的交互结果,再对照一条主网交易的 input。