代理/实现模式下合约插槽索引计算

我们知道,以太坊上的合约具有不可更改特性(代码即法律),那么什么是不可更改呢?其实这是指部署的合约的字节码无法再更改了,但不代表合约的实现逻辑是无法更改的。

一、什么是代理/实现模式

那么怎么更改合约的实现逻辑呢?有一个办法是把合约的实现逻辑外包给一个独立合约,执行时调用这个外部合约。需要更改逻辑时,首先更改外部合约逻辑重新部署一个合约,然后在主合约中重新设置该外部合约的地址即可。

采用上面这种办法其实相当于实现了合约的可升级功能,当前流行的代理/实现模式就是这样。主合约只是一个代理(Proxy)合约,所有的逻辑(其实是调用主合约不存在的函数)通过委托调用(delegateCall)的方式全部走实现合约。需要升级时更换一下实现合约地址就可以了。

二、合约的数据存储

但因为实现合约在编写和编译时操作的是它自己定义的数据结构,那么代理合约(主合约)不存在这些数据结构怎么办?这个其实不用担心,因为数据结构(例如变量名称)只是我们编写时使用的,编译部署后在底层EVM调用时是直接根据插槽位置读取内容的,EVM根本不用关心变量名称,也不会知道变量名称。

以太坊底层其实是一个K/V型数据库,不管K还是V都是32字节大小。这里的KEY就是插槽,编号从0开始,一直到2**256-1。

三、解决插槽共享冲突

通常来讲,合约存储状态变量都是从0插槽位置开始存储的,这样的话,如果代理定义了一个状态变量,实现合约也定义了一个不同的状态变量,都从0插槽开始存储,在操作时势必共享插槽位置引起冲突。因此,解决这种冲突的办法就是主合约(Proxy)尽量不定义状态变量,所有的状态变量都在实现合约中定义,这样就解决这个冲突问题了,但主合约至少要定义一个实现合约地址的状态变量,例如为implement

同时,为了防止任何人都可以升级合约设置新的implement,Proxy合约还需要一个admin状态变量,这样,通常意义上,Proxy 合约会有两个状态变量,如果默认的从0插槽位置开始存储,就会发生上面所说的冲突,怎么解决呢?

进一步的解决办法就是指定Proxy合约的两个状态变量的插槽位置,不再是默认的0和1。

四、计算插槽位置(索引)

那我们给adminimplement 这两个状态变量指定哪个位置(索引)的插槽比较合适呢?通常来讲,这个插槽索引不能太小,否则也会引起冲突。

这里就涉及到了一个eip1967,Standard Proxy Storage Slots。有兴趣的读者可以看一下:https://eips.ethereum.org/EIPS/eip-1967

从该EIP文档中我们可以看到,计算implementation插槽位置时,使用bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
而计算admin插槽位置时,使用bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)
可以看到,这两者只是计算字符串的最后部分不同,分别代表了不同状态变量。

当然,我们还可以增加一些角色,例如'eip1967.proxy.operator' 来代表operator状态变量等等。

有人说,使用这样的方法每个Proxy合约的admin插槽不都是一样的么?的确是这样,而有时为了保密的原因(至于为什么要保密我们以后再讲),我们可以自定义这个字符串的内容,比如改为'eip1967.vaultStorage.implementation'),这样,得到的插槽位置就和EIP-1967推荐的不一样啦。

那么问题来了,如果我们改成了自定义的字符串,EIP-1967中的计算方法是使用Solidity计算的(需要部署运行,有些麻烦)。那我们怎么线下提前计算出来,例如计算operator的插槽时使用'eip1967.proxy.operator' 来计算。我们接下来介绍线下计算的方法。

五、使用Python线下计算插槽位置

有一个方便的计算工具,是使用Python,首先需要安装web3.py

pip install web3

具体计算代码如下:

from web3.auto import w3

IMPLEMENTATION_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'
ADMIN_SLOT = '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103'
BEACON_SLOT = '0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50'
OPERATOR_SLOT = '0x14cc265c8475c78633f4e341e72b9f4f0d55277c8def4ad52d79e69580f31482'

def calSlot(str):
    kec = w3.keccak(text=str)
    num = w3.toInt(kec) -1
    result = w3.toHex(num)
    return result


assert(IMPLEMENTATION_SLOT == calSlot("eip1967.proxy.implementation"))
assert(ADMIN_SLOT == calSlot("eip1967.proxy.admin"))
assert(BEACON_SLOT == calSlot("eip1967.proxy.beacon"))
assert(OPERATOR_SLOT == calSlot("eip1967.proxy.operator"))

print("success")

这其中implementation、admin及beacon均是直接使用上述EIP-1967文档中的值,可见我们的计算是正确的。

六、使用Node.js来计算

既然涉及到区块链,怎么少得了javascript,和以太坊交互最方便的语言,没有之一。
我们使用ethers.js来进行计算,怎么安装依赖和运行js文件我这里就不再叙述了。例如yarn add ethers

直接上代码:

const { utils, ethers } = require("ethers");

const IMPLEMENTATION_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'
const ADMIN_SLOT = '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103'
const BEACON_SLOT = '0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50'
const OPERATOR_SLOT = '0x14cc265c8475c78633f4e341e72b9f4f0d55277c8def4ad52d79e69580f31482'

function calSlot(str) {
    const kec = utils.id(str)
    const num = ethers.BigNumber.from(kec)
    return num.sub(1).toHexString()
}

//这里使用console.log更好
console.assert(IMPLEMENTATION_SLOT === calSlot("eip1967.proxy.implementation"),"implementation")
console.assert(ADMIN_SLOT === calSlot("eip1967.proxy.admin"),"admin")
console.assert(BEACON_SLOT === calSlot("eip1967.proxy.beacon","beacon"))
console.assert(OPERATOR_SLOT === calSlot("eip1967.proxy.operator"),"operator")

文章的最后我们给出一个合约实例:

https://bscscan.com/address/0x4BfE9489937d6C0d7cD6911F1102c25c7CBc1B5A#code

希望这篇文章对从事区块链开发的读者有所帮助。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AiMateZero

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值