风险提示:理性看待区块链,提高风险意识!
如何实现可升级的智能合约?
首页 > 币界资讯 > 区块链知识 2018-09-18 11:55:00

智能合约的重要性已越来越明显,现如今,整个密码货币生态系统都是由智能合约所驱动!不管我们有多小心,或者我们的代码测试工作做得有多好,如果我们创建的是一个复杂的系统,那我们就有必要更新合约逻辑,以修补其存在的漏洞,或者添加必要的缺失功能。有时候,由于EVM虚拟机的更改或者被新发现的漏洞,我们可能需要去升级我们的智能合约。

一般来说,开发人员可以很容易地升级他们的软件,但区块链的情况是不一样滴,因为它们有着难以更改的属性。如果我们部署了一个合约,这就好比是泼出去的水。然而,如果我们使用适当的技术,我们可以在不同的地址部署一个新的合约,并使得旧合约无效。下面是一些最常见的,创建可升级智能合约的方法。

rt

主从合约(Master-Slave contract)

主从技术,是可实现升级智能合约最为基础也是最容易理解的技术之一。在这种技术当中,我们部署一个主合约,以及其他合约,其中主合约负责存储所有其他合约的地址,并在需要时返回所需的地址。当这些合约需要和其它合约进行沟通时,它们会充当从合约,从主合约那里获取其它合约的最新地址。为了升级智能合约,我们只需要在网络上部署它,并更改主合约中的地址。虽然这远不是发展可升级智能合约的最佳方式,但它确是最简单的。这种方法存在着很多的局限性,其中之一是,我们不能轻易地把合约的数据或资产迁移到新合约中。

永久存储合约(Eternal Storage contract)

在这种技术当中,我们将逻辑合约和数据合约彼此分离。数据合约应该是永久并且不可升级的。而逻辑合约可以根据需要进行多次升级,并将变化通知给数据合约。这是一项相当基本的技术,并且存在着一个明显的缺陷。由于数据合约是不可升级的,数据结构中需要的任何更改,或数据合约中存在的漏洞,都会导致所有数据变得无用。这种技术的另一个问题是,如果逻辑合约想要访问/操作区块链上的数据,那么这个逻辑合约将需要进行外部调用,而外部调用会消耗额外的gas。通常情况下,这种技术会和主从技术相结合,以促进合约间的通信。

可升级存储代理合约

我们可通过使永久存储合约充当逻辑合约的代理,以此防止支付额外的gas。这个代理合约,以及这个逻辑合约,将继承同一存储合约,那么它们的存储会在EVM虚拟机中对齐。这个代理合约将有一个回退函数,它将委托调用这个逻辑合约,那么这个逻辑合约就可以在代理存储中进行更改。这个代理合约将是永恒的。这节省了对存储合约多次调用所需的gas,不管数据做了多少的更改,就只需要一次委托调用。

这项技术当中有三个组成部分:

  1. 代理合约(Proxy contract):它将充当永久存储并负责委托调用逻辑合约;
  2. 逻辑合约(Logic contract):它负责完成处理所有的数据;
  3. 存储结构(Storage structure):它包含了存储结构,并会由代理合约和逻辑合约所继承,以便它们的存储指针能够在区块链上保持同步;

p1

委托调用

该技术的核心在于EVM所提供的DELEGATECALL操作码,DELEGATECALL就像是一个普通的CALL 调用操作码,不同之处在于目标地址上的代码是在调用合约上下文中执行的,而原始调用的msg.sender以及msg.value将被保留。简单说,DELEGATECALL基本上允许(委托)目标合约在调用合约的存储中做它任何想做的事情。

我们将利用这一点,并创建一个代理合约,它将使用DELEGATECALL操作码委托调用逻辑合约,这样我们就可以在代理合约中保持数据的安全,同时我们可以自由地更改逻辑合约。

如何使用可升级存储代理合约?

让我们深入研究一下细节。我们需要的第一个合约是存储结构。它将定义我们需要的所有存储变量,并将由代理合约和执行合约所继承。它看起来会是这样的:


contract StorageStructure {
address public implementation;
address public owner;
mapping (address => uint) internal points;
uint internal totalPlayers;
}

我们现在需要一个执行/逻辑合约。让我们创建一个简单版的合约,在添加新玩家时不会增加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;
}
}

为了让合约生效,我们首先需要部署代理合约以及ImplementationV1合约,然后调用这个代理合约的upgradeTo(address)函数,同时pass掉我们的ImplementationV1合约地址。现在,我们可以忘记这个ImplementationV1合约的地址,并把代理合约的地址作为我们的主地址。

为了升级这个合约,我们需要创建一个新的逻辑合约实现,它可以是这样的:


contract ImplementationV2 is ImplementationV1 {

function addPlayer(address _player, uint _points)
public onlyOwner
{
require (points[_player] == 0);
points[_player] = _points;
totalPlayers++;
}
}

你应该注意到,这个合约也继承了存储结构合约(StorageStructure contract),尽管它是间接地。

所有的执行方案都必须继承这个存储结构合约,并且在部署代理合约后不得进行更改,以避免对代理的存储进行意外覆盖。

为了实现升级,我们在网络上部署这个合约,然后调用代理合约的upgradeTo(address) 函数,同时pass掉ImplementationV2合约的地址。

这种技术,使得升级合约逻辑变得相当容易,但它仍然不允许我们升级合约的存储结构。我们可以通过使用非结构化的代理合约来解决这个问题。

非结构化可升级存储代理合约

这是当前最先进的,可实现智能合约升级的方法之一。它通过保存合约地址以及在存储中固定位置所有者的方法,以实现它们不会被执行/逻辑合约提供的数据所覆盖。我们可以使用sload以及sstore操作码来直接读取和写入由固定指针所引用的特定存储槽。

此方法利用了存储中状态变量的布局,以避免逻辑合约覆盖掉固定位置。如果我们将固定位置设置为0x7,那么在使用前7个存储槽后,它就会被覆盖掉。为了避免这种情况,我们将固定位置设置为类似keccak256(“org.govblocks.implemenation.address”).

这消除了在代理合约中继承存储结构合约的需要,这意味着我们现在也可以升级存储结构了。然而,升级存储结构是一项棘手的任务,因为我们需要确保,我们所提交的更改,不会导致新的存储布局与先前的存储布局不匹配。

这项技术有两个组成部分。

1、代理合约:它负责将执行合约的地址存储在一个固定的地址当中,并负责委托调用它;
2、执行合约:它是主要合约,负责把我逻辑以及存储结构;

你甚至可以将这项技术用于你现有的合约,因为它不需要对你的执行合约进行任何更改。

这个代理合约会是这样子的:


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)
}
}
}

如何使用非结构化可升级存储代理合约?

使用非结构化可升级存储代理合约是非常简单的,因为这种技术几乎可以处理所有现有的合约。想要使用这种技术,你只需要遵循以下步骤:

  1. 部署代理合约和执行合约;
  2. 调用代理合约的upgradeTo(address)函数,同时pass掉执行合约的地址。

我们现在可以忘掉这个执行合约地址,然后把代理合约的地址作为主地址。

而要升级这个新实施的合约,我们只需要部署新的执行合约,并调用代理合约的upgradeTo(address) 函数,同时pass掉这个新执行合约的地址。就是这么简单!

让我们简单举个例子。我们将再次使用上述可升级存储代理合约中使用的同一逻辑合约,但是我们不需要用到存储结构。因此,我们的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;
}
}

下一步是部署这个执行合约以及我们的代理合约。然后,再调用代理合约的upgradeTo(address) 函数,同时pass掉执行合约的地址。

你可能注意到,在这个执行合约中,甚至没有声明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)函数,并同时pass掉我们新执行合约的地址。现在,我们的合约已演变为能够保持跟踪 totalPlayers,同时仍然为用户提供相同的地址。

这种方法是强大的,但也存在着一些局限性。主要关注的一点是,代理合约拥有者(proxyOwner)有太多的权力。而且,这种方法对复杂的系统而言是不够的。对于构建具有可升级合约的 dApp而言,组合主从合约以及非结构化可升级存储代理合约,会是更为灵活的一种方法,这也是作者所在的GovBlocks所使用的方法。

结论

非结构化存储代理合约,是创建可升级智能合约最先进的技术之一,但它仍然是不完美的。毕竟,我们并不希望dApp所有者对dApp具有不当的控制权。如果开发者拥有了这种权力,那这个dapp还能称之为去中心化应用吗?在这里,我建议读者可以阅读下Nitika提出的反对使用onlyOwner的论点。你也可以在GitHub上窥探到我们的代理合约。

希望这篇文章可以帮助你创建可升级的智能合约。

同时向Zepplin在代理技术方面进行的工作致敬。

欢迎你给出自己的看法。

上一篇: 以太坊数据结构与存储分析
下一篇: 学术向丨论十大比特币区块链数据嵌入方式
推荐专栏
web3首席知识博主
一位相信价值投资的币圈KOL。稳定盈利的缠论野生交易员 #BTC行情分析师 #价值投资 #链上数据分析
爱Web 3,爱生活,爱科技,爱炒币的老韭菜
热门币种
更多
币种
价格
24H涨跌幅
BTC比特币
¥264,723.74
37,091.22 USDT
+0.1%
ETH以太坊
¥14,416.22
2,019.90 USDT
-0.12%
USDT泰达币
¥7.20
1.01 USDT
0%
BNB币安币
¥1,625.40
227.74 USDT
+0.36%
XRP瑞波币
¥4.32
0.60460 USDT
+0.37%
USDC
¥7.14
1.00 USDT
+0.03%
SOLSolana
¥398.85
55.89 USDT
+1.54%
OKBOK币
¥398.61
55.85 USDT
-1.64%
ADA艾达币
¥2.68
0.37580 USDT
-1.16%
DOGE狗狗币
¥0.55160
0.07730 USDT
-1.52%
热搜币种
更多
币种
价格
24H涨跌幅
Terra Classic
¥0.00
9.402E-5 USDT
-18.95%
Gala
¥0.18
0.025374 USDT
-4.66%
dYdX
¥22.58
3.1918 USDT
-0.91%
比特股
¥0.05
0.006964 USDT
+4.28%
PancakeSwap
¥15.52
2.1936 USDT
-2.74%
Conflux
¥1.08
0.1524 USDT
-2.87%
Filecoin
¥31.45
4.4454 USDT
-0.69%
FTX Token
¥29.82
4.2155 USDT
+16.96%
Yield Guild Games
¥2.55
0.3608 USDT
-0.52%
Shiba Inu
¥0.00
8.14E-6 USDT
-2.51%
比特币
¥262,381.44
37091.22 USDT
+0.1%
比原链
¥0.07
0.010011 USDT
-4.38%
最新快讯
更多
汇丰、恒生、渣打、富邦华一四家外资银行入围首批“数字人民币”业务试点名单
2023-11-28 19:06:57
摩根大通和Apollo计划建立代币化“企业主网”
2023-11-28 19:03:57
Nansen2公测版本上线,新增链上数据异动、智能搜索等功能
2023-11-28 18:59:52
西班牙公民需在明年3月底前申报其海外平台上加密货币持仓
2023-11-28 18:53:43
Nansen2已公开测试
2023-11-28 18:53:38
dYdX基金会:主网启动以来超过1645万DYDX被质押
2023-11-28 18:52:07
NicCarter等比特币倡导者发文:比特币挖矿是清洁能源和平衡电网的关键工具
2023-11-28 18:47:58
下载币界网APP