上链ON-CHAIN BLOCK 17 / 26
卷三 · 合约工程BLOCK 17

可升级合约与代理模式


合约一旦部署,代码就钉死了——那发现 bug 或想加功能怎么办?答案是一个聪明的间接层:让用户永远和一个"永不改变的门牌"(代理)交互,而门牌背后指向的"真正逻辑"可以被替换。这背后正是第 12 章那个危险的 delegatecall。这一章讲清它的原理、UUPS/透明两种主流玩法,以及那些让人半夜惊醒的存储陷阱。

核心思想:分离"数据"与"逻辑"

把一个合约拆成两个:

Solidity · 代理的心脏
contract Proxy {
    address implementation;   // 指向当前逻辑合约

    fallback() external payable {
        address impl = implementation;
        assembly {
            calldatacopy(0, 0, calldatasize())
            // ★ delegatecall:借 impl 的代码,在 Proxy 自己的 storage 上执行
            let ok := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch ok case 0 { revert(0, returndatasize()) }
                     default { return(0, returndatasize()) }
        }
    }
}

回顾第 12 章的关键性质:delegatecall逻辑合约的代码,操作的却是代理合约的存储。所以数据始终留在 proxy(升级不丢数据),而"怎么处理数据"的代码可以随时换。用户永远只认 proxy 的地址,毫无感知。

Android 类比

这几乎就是热更新 / 动态加载的思路。想想 Android 上的插件化 / 动态下发 dex:App 的壳(≈ proxy)保持稳定和数据,真正的业务逻辑(≈ implementation)可以后续替换。代理模式让"不可变的区块链"获得了一点点"可迭代"的能力——代价是复杂度和一整类新的风险。

两种主流模式

透明代理 (Transparent)UUPS
升级逻辑放哪在 proxy 里在 implementation 里
proxy 大小 / gas较大、每次调用略贵更小、更省 gas
风险点较稳妥若新实现漏了升级函数,永久锁死无法再升级
现状老项目多当前更推荐

实践中直接用 OpenZeppelin 的可升级库 + Foundry/Hardhat 插件,它们帮你处理绝大多数细节。但你必须理解底下发生了什么,否则一个存储错位就是灾难。

致命陷阱:存储布局冲突

这是可升级合约最容易翻车的地方。因为数据在 proxy、按 slot 存(第 10 章),而逻辑合约按自己的变量声明顺序去读写这些 slot。如果新版逻辑合约调整了变量顺序或在中间插入变量,slot 就会错位——新代码把 A 变量当 B 变量读,数据全乱。

Solidity · 升级时的存储灾难
// V1
contract LogicV1 { address owner; uint256 total; }
//               slot 0        slot 1

// V2 ✗ 在中间插了一个变量 → 全部错位!
contract LogicV2 { address owner; bool paused; uint256 total; }
//               slot 0       slot 1        slot 2 ← total 搬家了,读到的是旧 paused 位置的垃圾

规则:升级时只能在末尾追加变量,绝不能删除、重排、或在中间插入。专业做法是用带"预留空隙(storage gap)"的结构,或命名空间存储(ERC-7201)来彻底隔离。这类约束是可升级合约固有的税。

风险记账

可升级性是一把双刃剑,它本质上削弱了去中心化承诺:谁能升级,谁就能把逻辑换成"把所有钱转给我"。所以升级权限必须极度审慎——用多签、时间锁(升级要公示 N 天用户才有时间退出)、甚至最终放弃升级权(renounce)让合约真正不可变。用户在把大额资金放进一个可升级合约时,其实是在信任那个升级密钥的持有者。"可升级"约等于"有个后门,但但愿是好人拿着钥匙"。

什么时候不该用代理

可升级不是默认选项。它增加复杂度、gas、审计面和信任假设。很多场景更该追求不可变——比如一个纯粹的代币或一个博弈规则固定的协议,不可变本身就是卖点(用户知道规则永远不会被改)。选择前问自己:这个合约的价值主张,是"灵活可迭代",还是"承诺永不改变"?二者往往互斥。

要点入账

代理模式 = 用 delegatecall 把"固定的数据门牌(proxy)"和"可替换的逻辑(implementation)"分开,数据留在 proxy 所以升级不丢。核心风险有二:①存储布局必须严格向后兼容,只增不改;②升级权限就是后门,必须用多签/时间锁约束。可升级性和去中心化是一对张力,按项目定位取舍。卷三到此结束——你已经能读、写、测、审计合约了。卷四我们回到前端,把这些合约接进真实产品。

本章小结

◆ 返回目录