ERC-1967代理槽位怎么读? 图 1
ERC-1967代理槽位怎么读? · 图 1

查看代理合约时,地址、余额和用户状态都在代理上,业务代码却可能来自另一地址。ERC-1967用三个标准存储槽让工具定位实现、管理员或信标。读取槽位是起点,不是完整审计结论。

为什么槽位是“哈希再减一”

标准槽位来自特定命名字符串的keccak256结果减一。这样既不依赖Solidity普通变量的顺序,也降低与实现合约存储布局碰撞的概率。RPC使用eth_getStorageAt(proxy, slot, blockTag)读取32字节值;地址通常在低20字节,前面的零只是填充。

槽位读到什么下一步
implementation当前逻辑合约地址读取代码、源码和代码哈希
admin代理管理员地址判断它是EOA、多签还是ProxyAdmin
beacon信标合约地址调用beacon.implementation()

三个槽位的完整常量

  • implementation:0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
  • beacon:0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50
  • admin:0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103

这三个值分别是bytes32(uint256(keccak256(命名字符串)) - 1)的结果。调用eth_getStorageAt时应把完整32字节常量作为slot参数;若结果代表地址,取返回值最右侧20字节,并先判断是否全零。不要在运行时用字符串临时计算后不核对常量,否则一个字符差异就会读取完全无关的存储位置。

implementation与beacon不应被简单同时解释。BeaconProxy通常在beacon槽保存信标,再由信标统一返回实现;一组代理因此可随同一个信标升级。直接实现槽则常见于Transparent或UUPS路径。

一次完整读取应固定区块

先记录chainId、代理地址和目标区块号;再读取三个槽并保存原始32字节;随后对得到的地址调用eth_getCode,确认不是空地址或EOA;最后查询Upgraded、AdminChanged、BeaconUpgraded等事件,核对槽位变化发生在哪个交易。若使用latest分多次读取,升级恰好发生时可能拼出不存在的组合。

admin槽为空不等于不可升级

Transparent代理通常由admin或ProxyAdmin控制升级,普通用户调用会委托给实现;管理员调用则由代理自身处理管理函数。UUPS把升级入口放在实现代码中,权限由实现的_authorizeUpgrade等逻辑决定,因此admin槽可能为空。判断UUPS风险需要检查当前实现代码、proxiableUUID和授权角色,而不是看到空槽就下结论。

Beacon模式则要继续追踪谁能升级beacon。即便单个代理没有管理员,信标所有者仍可一次改变整组代理的逻辑。管理员若是多签,还要核对阈值、成员、模块和延迟执行;若背后再是代理,需要继续展开。

取证结论应写成一条链

可靠记录应包含:代理地址在固定区块的代码哈希;三个ERC-1967槽的原值;实现或信标地址的代码哈希;升级控制者;最近升级事件;源码验证状态。任何非标准字节码、Diamond结构或自定义槽都应标为“未能由ERC-1967解释”,而不是强行归类。

槽位计算与变量碰撞可继续看Solidity存储布局,实现内部的接口声明可用ERC-165探测辅助,固定区块的账户与存储证明可参考eth_getProof验证