先问一句“你是代理吗”:ERC-897 给升级型合约定的两个查询问题 图 1
先问一句“你是代理吗”:ERC-897 给升级型合约定的两个查询问题 · 图 1

先问一句“你是代理吗”:ERC-897 给升级型合约定的两个查询问题

把一个 NFT 系列的合约地址贴进区块浏览器,显示合约已验证,然后你发现它的逻辑可以整块换掉——这就是代理合约。ERC-897 创建于 2018 年 2 月 21 日,仓库记录状态为 Stagnant,它的目标非常克制:不标准化代理的实现方式,只标准化代理的自我声明,让任何钱包或审计脚本都能用两个函数问出答案。

两个函数的语义

接口只有一个:ERCProxy。第一个问题 proxyType() 是纯函数,返回代理类型编号:返回 1 是转发型代理,规则是只要实现地址非零,这个地址永远不变;返回 2 是可升级代理,实现地址可以按治理流程更换;返回 0 或者调用直接失败,则按约定视为这个地址不是(声明意义上的)代理。第二个问题 implementation() 返回此刻代理会转发到的代码地址。原文特意强调 proxyType() 必须是 pure,就是为了防止一个地址在不同时刻对同一个问题给出不同答案,把身份判定建立在可变电量的基础上。

先问一句“你是代理吗”:ERC-897 给升级型合约定的两个查询问题 图 2
先问一句“你是代理吗”:ERC-897 给升级型合约定的两个查询问题 · 图 2

查代理时它管不到的部分

ERC-897 回答的是“是不是、指向谁”,不回答“谁能改指向”。而风险恰恰集中在后半部分:可升级代理的升级函数可能在代理上,也可能被搬进实现合约;升级可能由单一多签触发,也可能绑定时间锁。标准自己也说,鉴于代理实现足够简单,标准化实现没有价值——于是把升级权限留给项目方自选。这意味着检查流程的正确顺序是:先用 proxyType()implementation() 判定结构,再去实现合约和代理合约两头查升级函数的调用权限、触发者的地址构成和生效延迟。另一类工具性边界也值得记录:大量真实代理并不实现这个接口,调用返回 0 只能证明它没有按 ERC-897 声明,不能证明它不是代理;以 EIP-1967 固定存储槽存实现地址的方案今天更常见,两者并不冲突,一个用固定槽位,一个用可调用接口,成熟的检查器会两个都试。

工具视角的两步扫描

一个成熟的安全检查器处理任意地址的顺序值得复述。第一步探测:对目标地址发一个 proxyType() 静态调用,调用失败或返回 0 只说明它没有实现这个接口,不能就此判定非代理;返回 1 或 2 才拿到结构结论。第二步交叉验证:把 implementation() 返回值和区块浏览器标注的实现合约、审计报告里的地址放在一张表里对比,三方一致才继续,不一致就说明逻辑地址发生过迁移。EIP-1967 那一类把实现地址放进固定存储槽的方案不实现本接口,检查器要按槽位读取补充判断,槽位数值以标准文本为准。

克隆代理与升级权限的分界

原文把代理技术拆成两个动机:升级逻辑,和为重复部署省钱的克隆。克隆型代理指向永远不变,风险重心在创建者名单;升级型代理的重点则完全在“谁能改指向”:升级函数可能留在代理合约上,也可能收进实现合约,触发者可能是单一地址、多签或治理合约,生效前是否延迟也不同。ERC-897 刻意不碰这些,因为它认为代理结构本身简单到不值得统一,统一的应当只有问询接口。这留下一个读者必须自己完成的动作:接口确认了可升级,还要把升级事件的触发者地址、时间戳和当前生效延迟逐项记录,才构成对某个 NFT 系列合约的完整权限画像。

对普通用户的意义

对准备铸造或持有某个 NFT 系列的人,这条接口给出的操作性结论是:把合约地址放进支持代理检测的浏览器插件或检查脚本,先拿到“是否代理、类型、实现地址”三要素,实现地址若和公开审计报告对不上,说明逻辑换过而文档没跟上;类型声明为可升级时,把“上一个升级事件是谁触发的、隔了多久生效”当作常规询问。标准停滞不等于接口失效,主网至今有存量合约用它自报身份,读它仍然有核对价值。

本文为机制说明,不构成任何投资建议。