本文主要说明以太坊的注册表合约、代理合约、继承的 存储 可升级性,以及更多的可升级性方法。
在软件工程中,当发现新的bug和安全风险时,通常会对它们进行 修补 ,并实时推送更新的版本。在智能合约开发中,可升级性并不是那么简单。因此,我们必须采取不同的做法。
以太坊仍处于起步阶段,关于如何升级智能合约版本的争议很多,但我们将介绍一些当今最好的选择。
注意:智能合约版本的可升级性仍然是研究的活跃领域。以下任何一种方法都可能由于滥用或新发现的漏洞而导致智能合约失败。
智能合约可升级性的基本方法
在这里,我们将介绍一些更平易近人但不太适合的智能合约可升级性解决方案。尽管这些不是最佳方法,但它们是当今使用的核心。
注册合约
注册表合约可能是最简单的可升级性方法,但是在这种方法,简单性带来了一些严重的缺陷。
它使用两个智能合约的工作:注册表合约和逻辑合约。注册表协定仅用于将用户指向逻辑协定的当前版本。每当逻辑合约被升级时,注册表合约的所有者就可以更新逻辑合约被升级的地址。
contract SomeRegis te r {
address backendContract;
address[] previo usB ackends;
address owner;
func ti on SomeRegister() {
owner = msg.sender;
}
mod if ier onlyOwner() {
require(msg.sender == owner)
_;
}
func TI on changeBackend(address newBackend) public
onlyOwner()
returns (bool)
{
if(newBackend != backendContract) {
previousBackends.push(backendContract);
backendContract = newBackend;
return true;
}
return false;
}
}
这种方法是非常不利的,因为当用户想要使用合约时,他们必须首先查找当前地址。否则可能导致资金损失。将数据迁移到新合约中也非常困难,因此必须仔细考虑此过程以避免失败。
代理合约
代理合约用于将数据和调用转发到逻辑合约。使用代理合约,用户可以始终调用相同的合约地址,并且将其简单地转发到当前逻辑合约。
这种方法通过使用DELEGATECALL操作码来工作。DELEGATECALL是EVM提供的用于程序集的操作码。它的工作方式与普通调用类似,只是目标地址的代码是在调用协定的上下文中执行的。这意味着像“msg.sender”和“msg.value”这样的值将被保留。实际上,DELEGATECALL允许目标协定代表被调用方进行调用。
contract Relay {
address public currentVe rs ion;
address public owner;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
func TI on Relay(address initAddr) {
currentVersion = initAddr;
owner = msg.sender; // this owner may be another contract with mul TI sig, not a single contract owner
}
func TI on changeContract(address newVersion) public
onlyOwner()
{
currentVersion = newVersion;
}
function() {
require(currentVersion.delegatecall(msg.data));
}
}
尽管这种方法避免了与注册表合同有关的问题,但它也有其自身的问题。 例如如果管理不当,数据存储很容易失败。如果新合约的存储布局与以前的合约不同,则数据可能已损坏。此实现还防止您从函数接收返回值,从而限制了其用例。
储存合约
与以前的方法一样,此方法需要您的逻辑合约以及辅助合约。在这种情况下,辅助合约是永久存储合约。该技术通过分离逻辑和数据来起作用。逻辑合约可以随时升级,并且由于数据存储在外部,因此您的数据受到保护。
当然,这种方法也存在根本缺陷。如果在存储合约中发现错误或漏洞,则在不破坏当前数据存储的情况下无法对其进行升级。 这种方法的另一个问题是逻辑协定需要使用额外的气体来进行外部调用以查看或修改数据。
更合适的升级方法
现在让我们来看看一些更复杂、更合适的智能合约升级方法。
继承的存储可升级性
这种技术使用三种不同的合约:代理合约来委托调用并充当永久存储;逻辑合约将处理数据;还有存储合约。代理合约和逻辑合约都继承自存储合约,因此它们的存储引用是对齐的。
当逻辑合约更新时,我们只需要更改代理合约所指向的位置即可使用仅管理员功能。由于代理和逻辑协定具有相同的存储指针,因此无需进行外部调用即可查看和修改数据。
不幸的是,这种方法也有其自身的陷阱。由于代理合约和存储合约都是永恒的,因此,如果在任何一个合约中发现错误或漏洞,都无法修复。 因此务必仔细考虑您的代理和存储结构。
非结构化存储可升级性
非结构化存储可能是当前最大的可升级性方法,它使我们能够利用存储中状态变量的布局。此方法仅需要两个合约-代理合约和实施合约-实施合约包含数据和存储。
该技术的工作原理是将可升级性所需的数据保存在存储中的固定位置,以防止被新数据覆盖。我们可以使用SLOAD和SSTORE操作码进行汇编。由于存储插槽只是从0x0开始递增,因此我们使用很高的存储插槽来防止覆盖 我们可以通过对常量变量进行散列来生成存储槽。 由于恒定状态变量不会占用存储空间,因此我们不必担心它会被覆盖。
bytes32 private constant implementationPosition =
keccak256(“org.zeppelinos.proxy.implementation”);
由于代理不再从存储合约继承而来,因此我们现在也可以更新存储,从而防止存储错误/漏洞变成灾难性的。 但是在升级实施合约时,我们必须继承以前的合约。由于不需要更改实施合约,因此该方法甚至可以与现有合约一起使用。
尽管这可能是当前可升级性最好的方法,但也有不少批评。代理所有者拥有巨大的权力,并且需要一定程度的信任。对于更复杂的系统,这可能也不是合适的解决方案。
升级依赖于构造函数的合约
当使用依赖于构造函数的合约来设置一些初始状态时,与代理工作并不太简单。由于构造函数只运行一次,而代理不知道逻辑合约构造函数中设置的值,因此我们需要一种方法在代理中初始化其中的一些值。
创建逻辑合约后,EVM会丢弃构造函数,因此我们不能简单地重用代码。相反,我们必须采取独特的方法来解决此问题。
初始化函数
一种可能的替代方法是在常规函数中使用构造函数代码。我们只需确保这个函数(我们将调用初始化函数)只能运行一次。
contract Initializable {
/**
* @dev Indicates that the contract has been initialized.
*/
bool private initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private initializing;
/**
* @dev Modifier to use in the initializer function of a contract.
*/
modifier initializer() {
require(initializing || !initialized, “Contract instance has already been initialized”);
bool wasInitializing = initializing;
initializing = true;
initialized = true;
_;
initializing = wasInitializing;
}
}
在使用初始值设定项函数时,必须打起十二分精神。考虑逻辑合约继承的基本合约也很重要。这部分特别复杂,因为Solidity也支持多重继承。
结论
确保智能合约是可升级的,并仔细考虑可升级过程,这两点都很重要。虽然这并不是一个关于智能合约可升级性的选项的详尽列表,但这应该是关于这个主题的适当指南。
责任编辑:ct