
用户交互的是代理入口,实现地址可以替换
OpenZeppelin《Proxy Upgrade Pattern》把升级写成两份合约:用户直接打交道的是包装层(proxy),它负责把交易转发给第二份含业务逻辑的合约。文档的关键表述是:逻辑合约可以被替换,而作为访问入口的代理本身不更换。两份合约的代码仍不可改写,只是代理可以改指向另一份实现。因此在“区块链代理合约如何对应资产”的阅读里,持有人日常看到的代币合约地址,通常是这份入口地址,而不是某一版实现合约的部署地址。
ethereum.org《Upgrading smart contracts》把代理模式写成:用户与存储数据的代理交互,代理保存逻辑合约地址,并用delegatecall把函数调用交给逻辑合约。页面同时强调,升级性与可变性不是一回事——已部署到某地址的程序仍不能被改写,但用户与该地址交互时实际执行的代码可以换成另一份。这解释了为什么区块浏览器里“代币合约地址”可以多年不变,界面功能却可能随实现版本变化。
delegatecall执行的是代理里的状态
OpenZeppelin在转发示例中写明,代码使用EVM的delegatecall,在调用者的状态上下文中执行被调用合约的代码。也就是说,逻辑合约操控的是代理的状态,逻辑合约自己的存储没有业务含义。代理不仅转发交易,还代表这一对合约的状态:状态在代理,逻辑在代理当前指向的实现里。
ethereum.org引用Solidity文档:delegatecall与普通消息调用的差别在于,目标地址的代码在调用合约的上下文中执行,msg.sender和msg.value不变;存储、当前地址和余额仍指向调用合约,只有代码来自被调用地址。因此,余额、授权额度这类资产账本如果写在代理存储里,换一份实现只是换解释这些槽的代码,并不会自动把账本搬到新实现地址。本篇到此为止,不展开槽位重排或类型打包如何破坏旧余额解释——那是另一篇合约升级布局核对应回答的问题。

同一入口对应不同逻辑版本时要分开核对
把上面两层合起来:入口地址稳定,是用户识别“这份资产合约”的常用线索;实现地址可变,用来说明当前业务逻辑版本。OpenZeppelin还指出,代理的回退函数可以把任意函数、任意参数动态转发给逻辑合约,不必为每个新接口在代理上做一对一映射,所以接口本身也可以随实现升级。核对某代币时,应同时问:交互的是不是代理入口、当前实现指向哪一版、账本是否仍由该入口的存储解释。
不能只因为实现合约换了地址,就宣布旧入口上的余额已经失效;也不能只因为入口没变,就默认新逻辑已正确解释全部历史余额。本篇只说明代理如何把“用户入口、业务逻辑、余额存储”分成三处观察,不给出某项目的升级操作步骤,也不把任一浏览器插件显示的实现地址当成资产已经迁移的证明。