代理/实现模式下合约插槽索引计算
Posted Zero_Nothing
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了代理/实现模式下合约插槽索引计算相关的知识,希望对你有一定的参考价值。
我们知道,以太坊上的合约具有不可更改特性(代码即法律),那么什么是不可更改呢?其实这是指部署的合约的字节码无法再更改了,但不代表合约的实现逻辑是无法更改的。
一、什么是代理/实现模式
那么怎么更改合约的实现逻辑呢?有一个办法是把合约的实现逻辑外包给一个独立合约,执行时调用这个外部合约。需要更改逻辑时,首先更改外部合约逻辑重新部署一个合约,然后在主合约中重新设置该外部合约的地址即可。
采用上面这种办法其实相当于实现了合约的可升级功能,当前流行的代理/实现模式就是这样。主合约只是一个代理(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。
四、计算插槽位置(索引)
那我们给admin
和implement
这两个状态变量指定哪个位置(索引)的插槽比较合适呢?通常来讲,这个插槽索引不能太小,否则也会引起冲突。
这里就涉及到了一个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
希望这篇文章对从事区块链开发的读者有所帮助。
以上是关于代理/实现模式下合约插槽索引计算的主要内容,如果未能解决你的问题,请参考以下文章