可升级合约
可升级合约
合约升级包括三种方式(目前)
参数化升级
这是升级智能合约最简单的方法,实际上,他并没有升级智能合约,因为我们不能真正的改变合约的逻辑。升级只是参数化一切
这种升级方法,是在合约部署之前,就在合约中实现了各种逻辑,当需要进行“升级”的时候,调用相关的函数即可,但是,如果在第一次部署智能合约时没有考虑到某些逻辑或功能,那将无法使用参数化来更新逻辑或更新任何内容。当然,这意味着要考虑很多事情:
- 谁是管理员?
- 谁可以访问这些函数?
如果是一个人,那么则得到了一个中心化合约,违背了智能合约的基本思想,当然可以添加一个治理合约,让他成为你的协议的管理员合约,这将是一种去中心化的方式的来实现的
社会迁移
通过建立一个新的合约(拥有了一个新的合约地址),然后通知之前所有在原合约上的人,迁移到新合约上(并且,mapping 中的一些值也要全部重新在新合约中再次记录)这十分复杂,需要进行很多社会惯例工作
Trail of Bits写了一篇关于 如何从A-V1 升级到 A-V2 等的升级协议的博客,其中提供了许多将存储和状态变量迁移到新合约的步骤: https://blog.trailofbits.com/2018/10/29/how-contract-migration-works/
Proxy(真正的升级)
最简单的合约用了许多低级功能,最主要的是委托调用(Delegatecall)
在谈到代理时,有四个很重要的概念:
The Implementation Contract(实施合约)
具有我们所有的逻辑和协议的所有部分,每当我们升级时,会启动一个全新的实施合约
The Proxy Contract (代理合约)
代理合约指向哪个实施是正确的,并将每个人的调用路由到正确的实施合约(可以认为,代理合约基于实际合约而实现)The User
用户将通过代理合约进行合约函数调用The admin
admin 是决定什么时候升级和指向哪个合约的人
这个场景中,代理和Delegatecall 的优点是:所有我的存储变量将存储在 代理合约 中,而不是在实施合约中,当我们要升级到新的逻辑合约时,我们所有的数据将保留在代理合约中。这样,如果我们需要更新合约的逻辑,只需指向新的实施合约,如果我们要添加新的存储变量或新的存储类型,只需在我的逻辑合约中添加它,代理合约能够捕获。
代理合约存在的问题
还是需要一个 DAO
如果还想满足合约的去中心化,那么就需要一个 DAO 了,否则,只由一个用户来决定什么去升级合约,那么就变成了一个中心化合约了。
两个最大的问题:
- 存储冲突(Storage Clashes)
- 函数选择器冲突(Function Selector Clashes)
存储冲突
当逻辑合约中涉及到修改状态变量的操作时,代理合约修改状态变量是按照插槽位置修改状态变量,而不是通过变量名。这意味着:我们只能在逻辑合约中添加新的存储变量,而不能重新排序或者更改旧的存储变量
函数选择器冲突
当我们告诉我们的代理合约将 Delegatecall 一个实例的时候,它是使用函数选择器来查找函数的,函数选择器是函数名称和函数签名的哈希值来决定的。有可能逻辑合约中的函数具有和代理合约中管理函数相同的函数选择器,这将会导致发生一些意外操作
这就引出了代理合约的三种实现中的第一种
代理合约的三种类型
透明代理合约(Transparent Proxy Pattern)
在这种方法中,只允许管理员调用管理函数,并且管理员不能调用逻辑合约中的任何函数。用户只能调用实施合约中的函数,而不能调用任何管理函数。这样就不会意外的交换二者地址,产生函数选择器冲突
通用升级协议(Universal Upgradeable Proxies - UUPS)
这种可升级合约的版本将所有升级逻辑放在实施合约本身,这样,Solidity 编译器会报错:你含有两个函数选择器相同的函数。
这种升级方式是有利的,因为我们少了一次读取,节省了 Gas,我么不需要在代理合约中检查某人是否是管理员。并且由于这个原因,代理合约也会变小。问题在于,如果你部署了一个没有可升级功能的实施合约,那么将受阻,并且回到社会迁移(合约迁移)的过程。
钻石代理合约(Diamond Proxy)
它做了很多事情,最重点的是,它允许多个实施合约。这解决了几个不同的问题
- 如果你的合约很大,无法适应一个合约的最大尺寸,你可以通过这种实施方法拥有多个合约
- 它还允许你对合约进行更精细的升级(比如不必总是不是和升级整个合约)