2、区块链与智能合约基础:以太坊原理、账户模型、交易与Gas、智能合约生命周期

智能合约的安全审计,本质上是对“运行在以太坊虚拟机(EVM)上的不可篡改业务逻辑”进行风险排查。因此,理解以太坊的底层运行机制是审计工作的前提。本章将围绕以太坊的核心原理、账户模型、交易与Gas机制以及智能合约的生命周期展开,为后续漏洞分析建立必要的知识框架。

以太坊核心机制与智能合约生命周期总览 账户模型 • EOA:私钥控制,发起交易 • CA:合约代码+存储,被动执行 • 状态:nonce、balance、storageRoot • 存储:256bit 键值对,Merkle Patricia Trie 交易与Gas • 交易字段:nonce, to, value, data • Gas = GasUsed × GasPrice • 存储成本:SSTORE 20000/5000 • 安全影响:Gas耗尽、价格操纵 合约生命周期 • 创建:CREATE / CREATE2 • 执行:调用、委托调用 • 升级:代理模式、数据分离 • 销毁:SELFDESTRUCT(已弃用) EVM 执行核心组件 栈 Stack 1024深度,256bit槽位 算术/逻辑操作 内存 Memory 易失性,字节寻址 每次交易后清空 存储 Storage 持久化,键值对 成本最高(SSTORE) 审计视角:所有漏洞最终都反映为 EVM 执行路径上的异常状态变更

2.1 以太坊核心原理:从“世界计算机”到状态机

2.1.1 核心原理:基于交易的状态转换

以太坊本质上是一个基于交易的状态机(Transaction-Based State Machine)。初始状态(Genesis State)经过一系列交易的驱动,转换为新的状态。每个区块包含若干笔交易,每笔交易都会触发一次确定性的状态转换。审计人员必须理解:智能合约的每一次函数调用,都是一次状态转换的候选者,而漏洞往往就隐藏在这些状态转换的边界条件中。

组件说明审计关注点
状态(State)所有账户(EOA+CA)的余额、nonce、存储和代码的集合状态变更是否符合业务预期
交易(Transaction)由EOA签名的指令,包含接收者、数据、Gas限制等交易数据是否可被恶意构造
区块(Block)打包一组交易,包含父哈希、时间戳、难度等元数据时间戳依赖、区块哈希依赖
EVM以太坊虚拟机,执行字节码的沙盒环境操作码序列、Gas消耗、栈深度

2.1.2 判断方法:识别状态转换的“原子性”

在审计中,判断一个合约是否安全,需要追踪每一笔交易的状态转换路径。具体操作流程如下:

  1. 确定入口函数:找到所有 public 或 external 函数。
  2. 追踪状态变量:列出函数中所有读写的状态变量。
  3. 分析转换条件:检查状态转换是否依赖外部数据(如预言机、时间戳、区块哈希)。
  4. 检查原子性:确认状态转换是否在单笔交易内完成,是否存在跨交易的状态依赖。
审计经验:如果一笔交易的状态转换依赖“前一笔交易”的结果,且没有显式的顺序控制,则可能存在重入或竞态条件风险。

2.1.3 案例拆解:状态机漏洞的典型场景

以一个简单的众筹合约为例。合约状态包括 funding(筹款中)、successful(成功)、failed(失败)。初始状态为 funding。当筹款截止时间到达且金额达标时,状态转为 successful;否则转为 failed。

漏洞场景:如果合约允许在 funding 状态下反复调用 refund(),且未在退款后更新状态,攻击者可以通过重入攻击在状态未切换前多次提取资金。审计时需检查状态转换是否在资金操作之前完成。

2.1.4 常见误区与总结

  • 误区一:认为“状态变量是私有的就安全”。private 仅阻止其他合约读取,但链上数据公开可见。
  • 误区二:忽略“存储”与“内存”的区别。存储是持久化的,内存是临时的,错误使用可能导致数据覆盖。
  • 总结:以太坊状态机的核心是“确定性”——同一组交易在任何节点执行结果必须一致。任何破坏确定性的操作(如依赖 block.number 的随机数)都是审计重点。

2.2 账户模型:EOA 与合约账户的博弈

2.2.1 核心原理:两类账户的差异

以太坊账户分为外部账户(EOA)和合约账户(CA)。EOA 由私钥控制,可以发起交易;CA 由代码控制,只能被动响应交易。审计中需要特别关注账户间交互的权限边界。

属性EOA(外部账户)CA(合约账户)
控制者私钥持有者合约代码逻辑
发起交易可以不可以(只能通过内部交易)
存储仅 nonce 和 balance代码 + 存储(Storage)
审计重点私钥管理、签名验证权限控制、委托调用风险

2.2.2 判断方法:权限与身份验证

审计合约时,需要明确每个关键函数的调用者身份。操作流程:

  • 使用 msg.sender 判断直接调用者;
  • 使用 tx.origin 判断交易发起者(不推荐使用,存在钓鱼风险);
  • 检查合约是否使用 delegatecall,这会导致调用者上下文被篡改。
风险提示:tx.origin 作为身份验证机制是常见漏洞。如果合约使用 require(tx.origin == owner),攻击者可以诱导 owner 调用恶意合约,恶意合约再调用目标合约,此时 tx.origin 仍是 owner,从而绕过权限。

2.2.3 案例拆解:delegatecall 的上下文陷阱

假设有一个代理合约 Proxy 和一个逻辑合约 Logic。Proxy 通过 delegatecall 将调用转发给 Logic。由于 delegatecall 在调用者的上下文中执行,Logic 中修改的存储变量实际修改的是 Proxy 的存储。

攻击场景:如果 Logic 合约的存储布局与 Proxy 不一致(例如 Proxy 的 slot 0 是 owner,而 Logic 的 slot 0 是 balance),攻击者可以通过调用 Logic 的函数修改 Proxy 的 owner。审计时必须核对存储布局的一致性。

2.2.4 常见误区与总结

  • 误区:认为合约账户“不能主动做事”就安全。合约可以通过内部交易(如调用其他合约)间接发起操作。
  • 总结:账户模型的核心是“上下文隔离”。审计时要时刻问:当前代码运行在谁的上下文中?存储读写的是谁的存储?

2.3 交易与Gas:成本约束下的安全边界

2.3.1 核心原理:Gas 的计量与限制

每笔交易都包含 gasLimit(用户愿意支付的最大 Gas 量)和 gasPrice(每单位 Gas 的价格)。交易费用 = gasUsed × gasPrice。EVM 中每个操作码都有固定的 Gas 消耗,其中 SSTORE(存储写入)是最昂贵的操作之一。

操作码Gas 消耗说明
ADD/SUB3算术运算
SLOAD800(热)/ 2100(冷)读取存储
SSTORE20000(新增)/ 5000(修改)/ 2900(清零返还)写入存储
CALL700(基础)+ 额外调用其他合约
SELFDESTRUCT5000销毁合约(已弃用)

2.3.2 判断方法:Gas 相关的漏洞模式

审计中需要关注以下 Gas 相关风险:

  1. Gas 耗尽(Out of Gas):函数在循环中执行大量存储操作,导致交易失败。攻击者可能利用这一点使合约功能不可用。
  2. Gas 价格操纵:如果合约逻辑依赖 tx.gasprice,攻击者可以通过设置不同的 gasPrice 影响合约行为。
  3. 区块 Gas 上限:单个区块的 Gas 总量有限(约 3000 万),如果合约操作过于昂贵,可能无法被打包。
审计技巧:使用 gasleft() 检查剩余 Gas 是常见模式,但要注意 gasleft() 本身也会消耗少量 Gas。在循环中依赖 gasleft() 可能导致边界条件不一致。

2.3.3 案例拆解:循环中的 Gas 耗尽攻击

考虑一个批量转账合约,用户传入一个地址数组,合约循环为每个地址转账。如果数组长度过大,循环中的 transfer 调用会消耗大量 Gas,导致交易失败。

攻击场景:攻击者构造一个包含数千个地址的数组,使合约在循环中耗尽 Gas。虽然交易失败,但攻击者可以反复发起此类交易,造成合约的拒绝服务(DoS)。审计建议:限制数组长度,或使用“拉取模式”(让用户自行提取)替代“推送模式”。

2.3.4 常见误区与总结

  • 误区:认为 transfer 是安全的转账方式。虽然 transfer 限制 Gas 为 2300,但接收方是合约时仍可能失败。
  • 总结:Gas 不仅是成本问题,更是安全边界。审计时要评估每个函数的 Gas 消耗上限,确保在极端输入下不会导致不可用。

2.4 智能合约生命周期:从创建到销毁

2.4.1 核心原理:合约的创建与执行

合约的生命周期包括:创建(CREATE/CREATE2)→ 执行(调用)→ 升级(代理模式)→ 销毁(SELFDESTRUCT)。审计需要覆盖整个生命周期,而不仅仅是部署后的代码。

阶段关键操作审计要点
创建构造函数、初始化参数构造函数是否可被攻击者抢先调用(如 Uniswap 的 initialize 漏洞)
执行函数调用、内部交易权限控制、重入、整数溢出
升级代理合约、存储迁移存储布局冲突、升级权限
销毁SELFDESTRUCT资金锁定、意外销毁

2.4.2 判断方法:生命周期各阶段的审计清单

  • 创建阶段:检查构造函数是否设置正确的初始状态;是否使用 CREATE2 且地址可预测。
  • 执行阶段:检查所有 external 函数的访问控制;检查 fallback 和 receive 函数的行为。
  • 升级阶段:检查代理合约的 upgradeTo 函数权限;检查新旧合约的存储布局是否兼容。
  • 销毁阶段:检查 SELFDESTRUCT 是否可被外部调用;销毁后资金去向。
重要提醒:SELFDESTRUCT 在以太坊合并后已弃用(EIP-6780),但在审计旧合约时仍需关注。销毁合约会将剩余 ETH 强制发送到指定地址,且无法撤销。

2.4.3 案例拆解:代理合约的存储冲突

一个典型的可升级合约使用 Proxy + Implementation 模式。Proxy 通过 delegatecall 将调用转发给 Implementation。审计时发现:Proxy 的 slot 0 存储 owner,而 Implementation 的 slot 0 存储 totalSupply。

攻击场景:攻击者调用 Implementation 中的 setTotalSupply() 函数,实际修改的是 Proxy 的 owner 变量。如果 setTotalSupply 没有权限控制,攻击者可以直接篡改 owner。审计建议:使用 EIP-1967 标准存储槽位,避免与业务变量冲突。

2.4.4 常见误区与总结

  • 误区:认为合约部署后代码不可变就“安全”。通过代理模式,逻辑可以升级,但升级本身可能引入新漏洞。
  • 总结:生命周期审计的核心是“状态一致性”——确保合约在任何阶段的状态转换都符合预期,且不存在未授权的状态变更路径。
本章小结:以太坊的账户模型、交易与Gas机制、合约生命周期共同构成了智能合约的运行环境。审计人员需要从“状态转换”的视角审视每一笔交易,从“上下文”的视角分析账户交互,从“成本约束”的视角评估Gas风险,从“生命周期”的视角覆盖所有阶段。只有建立完整的底层认知,才能准确识别和评估智能合约中的安全漏洞。