Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

EVM 内存扩张模型与瞬态存储 (EIP-1153)

在 EVM 性能调优中,开发者往往将全部精力投入到持久化存储(SSTORE / SLOAD)的优化上,却极易忽视内存扩展(Memory Expansion)的非线性成本以及 Cancun 升级引入的**瞬态存储(Transient Storage)**革命性优化。

内存与瞬态存储的 Gas 数字采用 Cancun 指令计费规则。实际成本取决于编译器、优化器、目标网络和完整调用路径。


EVM 内存管理机制与二次方成本模型

EVM 的内存(Memory)是一个线性的、按字节寻址的连续数组。与常规物理机内存可以自由释放(free())不同,EVM 内存只能单向向高地址追加扩展,在单次调用生命周期内绝不支持释放回收。

1. 内存扩展 Gas 计费公式

当内存使用量由 0 扩展至 \(a\) 个字(Word,每个字 32 字节)时,累积消耗的 Gas 为:

$$ C(a) = 3a + \left\lfloor \frac{a^2}{512} \right\rfloor $$

当分配较小内存时,线性项 \(3a\) 起主导作用;当分配规模持续增加时,二次方项 \(\frac{a^2}{512}\) 会引发 Gas 成本呈超线性急剧攀升:

内存大小 (Bytes)占用 Word 数 (\(a\))累积内存 Gas 成本扩展边际成本
32 B13 Gas极低
1 KB3298 Gas轻微
32 KB1,0245,120 Gas中等
256 KB8,192155,648 Gas较高;21,000 gas 是普通 ETH 转账的基础费用,不是交易上限
1 MB32,7682,195,456 Gas很高;能否执行取决于交易 gas limit 和网络限制

2. Solidity 内存默认布局与自由内存指针

Solidity 编译器固定保留了前 128 字节的内存空间:

  • 0x00 - 0x3f(64 字节):计算哈希专用的临时草稿空间(Scratch Space)。
  • 0x40 - 0x5f(32 字节):自由内存指针(Free Memory Pointer)。其存储的值即为当前可用内存的起始物理地址(系统初始化时为 0x80)。
  • 0x60 - 0x7f(32 字节):零槽位(Zero Slot),用作未赋值动态数组的默认初始引用。
// 危险的大循环内存泄漏示例
function processData(uint256[] calldata data) external pure {
    for (uint256 i = 0; i < data.length; i++) {
        // 严重缺陷:在循环内部反复 new 动态数组或拼接 bytes
        // 自由内存指针持续向右推进,触发高额二次方内存扩展惩罚
        bytes memory temp = abi.encodePacked(data[i], i);
        _consume(temp);
    }
}

防范策略:先限制输入规模,再考虑复用缓冲区以减少重复分配。汇编覆写必须保证长度、边界和自由内存指针正确;循环内分配不是必然错误,应按完整操作的 Gas 预算判断。


EIP-1153 瞬态存储 (Transient Storage) 深度实战

以太坊坎昆(Cancun)硬分叉引入的 EIP-1153 带来了两个全新的操作码:

  • TSTORE (0x5d):将 32 字节值写入指定的瞬态槽。
  • TLOAD (0x5c):从指定的瞬态槽读取 32 字节值。

1. 瞬态存储的核心行为特征

维度常规存储 (Storage)内存 (Memory)瞬态存储 (Transient Storage)
持久性永久持久化上链调用帧结束即销毁在当前整笔交易上下文有效,交易结束丢弃
调用域按所属合约隔离当前调用帧;internal 调用共享同一合约的调用帧共享;DELEGATECALL 使用调用者空间
读写 Gas冷 SLOAD 2100;SSTORE 取决于原值、现值、新值与访问温度指令费加扩展费Cancun 下 TLOAD / TSTORE 各 100 Gas
回滚行为回滚对应调用及其子调用的修改子调用的 memory 与父帧独立回滚对应调用及其子调用的瞬态修改

典型应用场景一:极简无冷态重入锁

传统的 ReentrancyGuard 依赖持久化 Storage:

  • 进入时执行 SSTORE 设置锁定标记(即使从 1 变 2,暖写也需 2900 Gas)。
  • 退出时再次 SSTORE 复位。
  • 消耗高达 3000 - 5000 Gas。

基于 TSTORE 的瞬态重入锁

以下为简化实现,要求 evm_version = "cancun" 及支持 EIP-1153 的网络。OpenZeppelin 的 ReentrancyGuardTransient 提供独立的重入锁实现。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

abstract contract ReentrancyGuardTransient {
    // 命名空间伪随机槽位
    bytes32 private constant REENTRANCY_GUARD_SLOT =
        0x8e94fed44239eb2314ab7a406345e6c5a8f0ccedf3b600de3d004e672c33abf4;

    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        assembly {
            if tload(REENTRANCY_GUARD_SLOT) {
                // 已处于锁定状态,直接回滚
                mstore(0x00, 0x3ee5aeb5) // ReentrancyGuardReentrantCall()
                revert(0x1c, 0x04)
            }
            // 加锁:仅消耗 100 Gas
            tstore(REENTRANCY_GUARD_SLOT, 1)
        }
    }

    function _nonReentrantAfter() private {
        assembly {
            // 解锁:仅消耗 100 Gas
            tstore(REENTRANCY_GUARD_SLOT, 0)
        }
    }
}
  • 成本边界:一次正常保护包含一次 TLOAD 检查和两次 TSTORE 写入,三个指令合计 300 Gas,还需加上判断、栈操作等成本。不能把两次写入的 200 Gas 当作整个修饰器的成本。
  • 清理边界:正常退出时仍需解锁,否则同一交易中后续合法调用会被阻止。交易结束时自动清除不能替代函数出口的清理。

典型应用场景二:Uniswap V4 单例闪电记账 (Flash Accounting)

在 Uniswap V3 中,每个交易对是一个独立的智能合约实例。每次代币兑换(Swap)必须在当前池内完成物理资产转移(Transfer),多跳路由(如 ETH \(\rightarrow\) USDC \(\rightarrow\) DAI)会导致频繁的 ERC-20 外部代币划转与 balanceOf 检查。

Uniswap V4 的瞬态记账范式

Uniswap V4 采用了单例架构(Singleton Pattern - PoolManager),所有池子集中在单一合约中管理:

  1. 整个交易流程引入“先欠账后结算”机制。用户在多跳兑换中产生的代币债务和盈余,全部记录在瞬态存储 TSTORE 中。
  2. 内部多跳计算完全是在瞬态存储中修改数字,不产生任何实际的 ERC-20 代币转账。
  3. 路由在解锁回调内通过结算与提取消除各币种的债务和盈余。PoolManager 在回调结束时检查所有 delta 归零;资产转账次数取决于路由与币种,不能概括为固定一次。
sequenceDiagram
  autonumber
  actor User as 交易用户 / 路由
  participant PoolManager as Uniswap V4 PoolManager (单例)
  
  User->>PoolManager: 发起多跳兑换 Swap(TokenA -> TokenB -> TokenC)
  Note over PoolManager: 步骤 1: 瞬态借记 TokenA (TSTORE 记录债务)
  Note over PoolManager: 步骤 2: 瞬态计算 TokenB 中间换算 (0 物理划转)
  Note over PoolManager: 步骤 3: 瞬态贷记 TokenC (TSTORE 记录盈余)
  Note over PoolManager: 终局结算: 仅在出口进行 1 次 TokenA 存入与 1 次 TokenC 提取
  PoolManager-->>User: 交付最终代币

参考文献与规范