如今智能合约的生态已经比较完善。无论我们在编码时多么的仔细,测试多么的严密,如果我们构建一个复杂的项目,那么很有可能我们会需要更新业务逻辑,给系统打补丁,修复bug或添加新的特性。有时,我们可能因为EVM的改变或因为新的漏洞的发现而需要更新我们的合约代码。
通常情况下,对于传统的开发项目来说,开发者可以随时更新代码。但是智能合约与此不同,它一旦部署完成之后代码就无法修改。然而,如果我们使用合适的技术,我们可以部署一个新的合约,并且抛弃旧的合约。如下是目前创建可升级合约的几种常见的开发模型。
Master-Slave contracts 主从模式
主从模式是最简单也最好理解的一种构建可升级合约的开发模型。在技术上来说,我们首先部署一个master合约。master合约里存放其他合约的合约地址,并在需要时返回所需的合约地址。作为slave的合约从master合约获取其他合约的最新地址并与之通信。在更新一个合约时,我们只需要重新部署一个新的合约,并在master合约中更新其地址。这种方法构建的可升级模型虽然简单,但并不是很完美,其中一个限制就是它无法将旧合约的数据迁移到新的合约之上。
Eternal Storage contracts 外部存储合约
在此模型中,我们将业务逻辑和数据分离成两个独立的合约。数据合约是固定的不可升级合约。业务合约可以根据需求随时更新,更新完成之后数据合约会收到通知。这种模型中存在一个很明显的缺点。因为数据合约是不可升级的,任何对于数据结构更改的需求或数据合约的bug都可能导致所有的数据失效。还有一点就是,业务合约如果需要访问或修改数据,那么它需要发起一个外部调用,这种调用会消耗更多的gas。这种模型通常与Master-Slave模型结合使用来促进合约的通信。
Upgradable Storage Proxy Contracts 可升级的存储代理合约
如果我们将存储合约作为业务合约的一个代理,那么就可以节省这笔额外的gas支出。代理合约和业务合约,会继承相同的存储合约,这样一来它们在EVM中对于存储的引用就是一致的。代理合约会有一个fallback
方法,此方法会通过delegate call
调用业务合约,这样子在代理合约的存储中就可以对业务合约做出改变。代理合约是固定不变的,它目前节省了针对存储合约的外部调用的gas,无论数据做出多少改变,它只需要发起一个delegate调用。
在这个模型中存在三个组件
- Proxy contract 代理合约:它作为外部存储和delegate调用业务合约。
- Logic contract 业务合约:业务逻辑,操作数据。
- Storage structure:数据结构基类合约,它同时被代理和业务合约所继承,以便保持存储指针在链上的一致性。
Delegate Call
这个技术模型的核心在于EVM提供的DELEGATECALL
opcode。DELEGATECALL
与普通的调用的区别在于,虽然真正执行的代码在被调用的目标地址上,但是代码执行的上下文确在调用合约上(msg.sender
和msg.value
的值保持原样,这是它与合约之间的external调用的最大区别)。因此,当使用DELEGATECALL
时,目标地址的代码被执行,但是Storage、address和调用合约的余额才是真正被使用的数据(不会使用目标合约中的数据)。换句话说,DELEGATECALL
允许目标地址在执行时使用调用合约中的存储。
我们可以利用这一优势来创建代理合约,它会DELEGATECALL
代理调用到业务合约,这样子我们可以将数据安全的保存在代理合约里面,然后根据需要更新我们的业务合约。
How to use upgradable storage proxy contracts? 如何使用可升级的存储代理合约?
接下来让我们往下研究的更深入一点。第一个合约需要被创建的是存储结构。它会定义所有我们需要的存储变量,并且这个合约会被代理和业务合约所继承。例如:
contract StorageStructure {
address public implementation;
address public owner;
mapping (address => uint) internal points;
uint internal totalPlayers;
}
接下来我们需要一个业务合约。让我们创建一个buggy实现,这个合约当新的玩家被添加的时候不会递增totalPlayers
。
contract ImplementationV1 is StorageStructure {
modifier onlyOwner() {
require (msg.sender == owner);
_;
}
function addPlayer(address _player, uint _points)
public onlyOwner
{
require (points[_player] == 0);
points[_player] = _points;
}
function setPoints(address _player, uint _points)
public onlyOwner
{
require (points[_player] != 0);
points[_player] = _points;
}
}
接下来是最重要的部分 —— 代理合约:
contract Proxy is StorageStructure {
modifier onlyOwner() {
require (msg.sender == owner);
_;
}
/**
* @dev constructor that sets the owner address
*/
constructor() public {
owner = msg.sender;
}
/**
* @dev Upgrades the implementation address
* @param _newImplementation address of the new implementation
*/
function upgradeTo(address _newImplementation)
external onlyOwner
{
require(implementation != _newImplementation);
_setImplementation(_newImplementation);
}
/**
* @dev Fallback function allowing to perform a delegatecall
* to the given implementation. This function will return
* whatever the implementation call returns
*/
function () payable public {
address impl = implementation;
require(impl != address(0));
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize)
let result := delegatecall(gas, impl, ptr, calldatasize, 0, 0)
let size := returndatasize
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
/**
* @dev Sets the address of the current implementation
* @param _newImp address of the new implementation
*/
function _setImplementation(address _newImp) internal {
implementation = _newImp;
}
}
接下来我们需要部署Proxy
和ImplementationV1
合约,然后调用代理合约的upgradeTo(address)
方法,传入的变量是部署完成之后的ImplementationV1
的地址。然后ImplementationV1
的合约地址就没用了,代理合约就是我们的主合约,接下来所有的交互通过代理合约来进行。
要更新合约的话,我们需要创建一个新的业务合约的实现,例如:
contract ImplementationV2 is ImplementationV1 {
function addPlayer(address _player, uint _points)
public onlyOwner
{
require (points[_player] == 0);
points[_player] = _points;
totalPlayers++;
}
}
这里需要注意的是,新的业务合约仍然继承了
StorageStructure
合约,虽然是非直接继承。
所有的业务合约都需要继承StorageStructure合约,并且Storage合约在代理合约部署完成之后就不应该再改变,以避免无意识的覆盖了代理合约中的数据
要更新新的业务合约,我们将ImplementationV2合约部署完成之后,调用代理合约的upgradeTo(address)
方法,传入ImplementationV2
合约地址作为参数。
这个技术模型允许我们非常方便的更新业务合约,但是它仍然不允许更新存储合约的数据结构。这一问题我们可以通过接下来介绍的unstructured proxy contracts
技术模型来解决。
Unstructured Upgradable Storage Proxy Contracts
这是目前构建可升级合约的最先进的技术模型。它把业务合约的地址和owner的地址存储在代理合约的固定的存储位置,以避免业务合约对其可能造成的覆盖。我们可以使用sload
和sstore
opcodes来直接读取和写入到指定的存储slot中,然后通过固定的锚点来引用它。
关于state variable在存储中的布局可以参考layout of state variables in storage。将数据存储在固定的slot中,可以避免其被业务合约覆盖。假如我们将fixed position设置为0x7
,它会被初始的7个存储slots之后的数据所覆盖。为了避免这种情况,我们将fixed position设置为这种形式:keccak256(“org.govblocks.implemenation.address”)
。
以这种技术模型来构建可升级合约时,代理合约不需要继承StorageStructure
合约,这意味着我们的StorageStructure
合约也是可升级的。但是升级storage struct是一项棘手的任务,因为我们需要确保新的改动不会造成新的存储布局和之前的存储布局之间的重叠。
There are two components of this technique
- Proxy Contract:它将业务合约的地址存储在一个固定的存储slot中,并通过
DELEGATECALL
调用它。 - Implementation Contract:业务合约,它包含了业务逻辑和存储结构。
这个技术模型不需要业务合约做任何改变,你甚至可以使用之前编写好的合约来构建成可升级的模型。
代理合约代码样例如下:
contract UnstructuredProxy {
// Storage position of the address of the current implementation
bytes32 private constant implementationPosition =
keccak256("org.govblocks.implementation.address");
// Storage position of the owner of the contract
bytes32 private constant proxyOwnerPosition =
keccak256("org.govblocks.proxy.owner");
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyProxyOwner() {
require (msg.sender == proxyOwner());
_;
}
/**
* @dev the constructor sets owner
*/
constructor() public {
_setUpgradeabilityOwner(msg.sender);
}
/**
* @dev Allows the current owner to transfer ownership
* @param _newOwner The address to transfer ownership to
*/
function transferProxyOwnership(address _newOwner)
public onlyProxyOwner
{
require(_newOwner != address(0));
_setUpgradeabilityOwner(_newOwner);
}
/**
* @dev Allows the proxy owner to upgrade the implementation
* @param _implementation address of the new implementation
*/
function upgradeTo(address _implementation)
public onlyProxyOwner
{
_upgradeTo(_implementation);
}
/**
* @dev Tells the address of the current implementation
* @return address of the current implementation
*/
function implementation() public view returns (address impl) {
bytes32 position = implementationPosition;
assembly {
impl := sload(position)
}
}
/**
* @dev Tells the address of the owner
* @return the address of the owner
*/
function proxyOwner() public view returns (address owner) {
bytes32 position = proxyOwnerPosition;
assembly {
owner := sload(position)
}
}
/**
* @dev Sets the address of the current implementation
* @param _newImplementation address of the new implementation
*/
function _setImplementation(address _newImplementation)
internal
{
bytes32 position = implementationPosition;
assembly {
sstore(position, _newImplementation)
}
}
/**
* @dev Upgrades the implementation address
* @param _newImplementation address of the new implementation
*/
function _upgradeTo(address _newImplementation) internal {
address currentImplementation = implementation();
require(currentImplementation != _newImplementation);
_setImplementation(_newImplementation);
}
/**
* @dev Sets the address of the owner
*/
function _setUpgradeabilityOwner(address _newProxyOwner)
internal
{
bytes32 position = proxyOwnerPosition;
assembly {
sstore(position, _newProxyOwner)
}
}
}
How to use unstructured upgradable storage proxy contracts?
使用unstructed upgradeable storage proxy contracts非常简单,因为此模型几乎可以与所有的现存的合约进行组合。通过如下的步骤来使用它:
- 部署代理合约和业务合约。
- 调用代理合约的
upgradeTo(address)
方法,调用时传递业务合约的地址作为参数。
接下来所有的交互就只需要与代理合约进行就可以了。并且交互方法与直接调用业务合约没有区别。
要更新业务合约,我们只需要部署一个新的业务合约,然后调用代理合约的upgradeTo(address)
方法,调用时将新部署的业务合约地址作为参数传递即可。就是这么滴简单!
让我们举一个栗子看看它是如何工作的。我们将会使用在上个技术模型讲解时使用的相同的合约,但是我们不需要继承storage structure
,那么我们的业务合约ImplementationV1
就是这样的:
contract ImplementationV1 {
address public owner;
mapping (address => uint) internal points;
modifier onlyOwner() {
require (msg.sender == owner);
_;
}
function initOwner() external {
require (owner == address(0));
owner = msg.sender;
}
function addPlayer(address _player, uint _points)
public onlyOwner
{
require (points[_player] == 0);
points[_player] = _points;
}
function setPoints(address _player, uint _points)
public onlyOwner
{
require (points[_player] != 0);
points[_player] = _points;
}
}
接下来我们需要部署此合约到链上,然后调用代理合约的upgradeTTo(address)
方法,将新的地址作为参数传递进去。
你可以注意到totalPlayers
变量在这个合约中是没有声明的。我们可以创建一个新的合约,在这个新的合约中包含totalPlayers
变量的声明的使用。新的合约如下:
contract ImplementationV2 is ImplementationV1 {
uint public totalPlayers;
function addPlayer(address _player, uint _points)
public onlyOwner
{
require (points[_player] == 0);
points[_player] = _points;
totalPlayers++;
}
}
升级到这个新的业务合约的步骤和上面一样,调用代理合约的upgradeTo(address)
方法,然后将新合约的地址作为参数传递进去即可。那么我们的合约就加上的追踪totalPlayers
的功能,同时对于用户来说调用的地址还是同一个(代理合约的地址)。
这个技术模型虽然强大,但是还是有一些限制。其中之一就是proxyOwner
拥有很大的权利。这个模型对于复杂的系统来说还不够用,将上面的Master-Slave
和unstructured upgradable storage proxy contract
模型结合使用对于构建可升级合约的dApp会更加灵活。这也是我们在GovBlocks上使用的技术。
Conclusion 总结
Unstructured Storage Proxy Contracts
模型是最高级的一种模型,但是它也并非完美。我们在GovBlocks中,不希望dApp的owner拥有绝对的权利。毕竟它们是去中心化的应用程序。所以,我们决定在我们的代理合约中使用网络范围的授权人,而不是使用单一的proxyOwner。我将会在未来的文章中介绍这种实现的方法。同时,我推荐阅读Nitika的文章argument against the use of onlyOwner,你可以在我们的github上查看我们的代理合约代码。
希望这篇文章对你有帮助!
推荐关注Zepplin对于这一块的技术研究。
完!
参考资料
原文:How to make smart contracts upgradable!
Proxy Patterns