﻿---
title: "代理存储冲突：升级如何破坏合约状态"
description: "代理保留状态，而实现代码可以变更。了解不兼容的存储布局如何覆写余额、所有者和升级控制，以及如何安全验证升级。"
image: "https://wiki.fcontext.com/og.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://wiki.fcontext.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 代理存储冲突：升级如何破坏合约状态

> 仅供教育参考，不构成投资建议；投资可能产生损失。

<a id="answer"></a>

## 直接答案

当实现代码以不同于现有状态创建时布局所赋予的含义，读取或写入代理的存储槽，就会发生代理存储冲突。升级可能因此把余额当作地址、清空所有者、破坏映射的根槽，或覆写升级控制数据。

使用 `delegatecall` 时，实现字节码在代理的上下文中执行：存储、余额和 `address(this)` 都属于代理。变量名不会存储在链上，因此 EVM 只按新代码计算出的槽位和字节偏移操作。

所以，升级必须保持已部署布局兼容，而不只是成功编译或公开相同函数。授权升级之前，应将编译器生成的布局与确切的已部署版本比较。

<a id="mechanism"></a>

## 工作原理

Solidity 通常在继承关系经过 C3 线性化后，按声明顺序从槽 `0` 起放置状态变量。小于 32 字节的值可能共用一个槽；结构体和数组还有额外的放置规则，而映射和动态数组会根据基础槽推导数据位置。基础槽一旦移动，其派生数据的位置也会改变。

需要审计以下四类不同的冲突边界：

- **代理与实现之间：** 实现地址、管理员等代理自有字段不得占用应用状态所用的槽。ERC-1967 为实现、信标和管理员数据规定了编译器常规分配不会触及的标准槽。
- **旧实现与新实现之间：** 现有应用变量必须保持兼容的槽、偏移和类型。在末尾追加变量可能安全，但插入、重排、删除或改变变量类型会重新解释已有存储字。
- **继承关系：** 向基类添加状态或改变继承顺序，可能移动派生合约声明的存储，即使派生合约源码没有变化。
- **预留或命名空间存储：** 正确使用存储间隙可以为基类预留空间，ERC-7201 风格的命名空间可以隔离布局。但两者都不允许任意修改既有布局内部。

<a id="example"></a>

## 示例

假设版本 1 的布局如下：

```solidity
uint256 totalAssets; // slot 0
address owner;       // slot 1
```

版本 2 错误地在开头插入一个变量：

```solidity
bool paused;         // slot 0, offset 0
uint256 totalAssets; // slot 1
address owner;       // slot 2
```

升级后，`paused` 会读取旧 `totalAssets` 的低位字节，新 `totalAssets` 会把旧 `owner` 存储字当作整数读取，而 `owner` 会读取槽 `2` 中原有的内容，通常为零。原始存储字仍然存在，但新代码赋予了它们不同含义。交易可能成功执行，却以错误解释后的状态进行授权或记账。

<a id="risks"></a>

## 风险与升级检查

- 为两个实现生成存储布局输出，并以实际部署的参考合约为准，比较槽、偏移、类型和继承信息。
- 对传统线性布局，只在末尾追加新变量。除非已证明兼容，否则不要重排现有字段、改变类型、删除后复用，也不要修改基类。
- 使用存储间隙时，应按实际占用的预留槽数精确缩减间隙。使用命名空间存储时，须确保命名空间标识唯一，并验证每个现有命名空间内的变更。
- 不要把 ERC-1967 当作完整保护。它将代理元数据与编译器分配的应用槽分开，但不能保证两个实现布局彼此兼容。
- 在分叉网络或状态快照上测试升级及任何重新初始化函数。执行前后都要核对所有者、角色、余额、授权额度、映射条目、暂停状态、实现槽以及回滚或紧急控制。

如果升级已疑似造成冲突，在治理权限允许时，应暂停后续升级和状态变更调用。保留升级前区块号与实现字节码，比较受影响槽位的原始存储，并由合格的合约工程师设计迁移方案、接受独立审查。没有经验证的存储映射就反复升级，可能破坏更多原本可恢复的状态。

<a id="misconceptions"></a>

## 常见误区

- **“变量名没变，布局就是安全的。”** 名称不决定存储位置，类型、顺序、打包、继承和命名空间规则才决定。
- **“删除变量就能释放槽位供复用。”** 代理存储会持续存在。除非经过严格审查的迁移将旧值清除或转换，否则复用槽位只是给旧存储字赋予新含义。
- **“一笔测试交易成功就证明升级兼容。”** 测试可能只触及少量槽位。布局验证和状态差异测试必须覆盖特权字段、打包值、映射、数组和继承存储。

<a id="related"></a>

## 相关主题

- [Delegatecall 存储风险](/zh-cn/crypto/delegatecall-storage-risk/)
- [初始化器接管](/zh-cn/crypto/initializer-takeover/)
- [代理合约](/zh-cn/crypto/proxy-contract/)
- [代理升级监控](/zh-cn/crypto/proxy-upgrade-monitoring/)
- [可升级合约](/zh-cn/crypto/upgradeable-contract/)

<a id="sources"></a>

## 来源

- [智能合约简介](https://docs.soliditylang.org/en/latest/introduction-to-smart-contracts.html) - Solidity Documentation（访问日期：2026-08-21）
- [存储和瞬态存储中状态变量的布局](https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html) - Solidity Documentation（访问日期：2026-08-21）
- [ERC-1967：代理存储槽](https://eips.ethereum.org/EIPS/eip-1967) - Ethereum Improvement Proposals（访问日期：2026-08-21）
- [编写可升级合约](https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable) - OpenZeppelin Documentation（访问日期：2026-08-21）

Source: https://wiki.fcontext.com/zh-cn/crypto/proxy-storage-collision/index.mdx
