跳转到主要内容
Change page

升级智能合约

以太坊上的智能合约是在以太坊虚拟机 (EVM) 中运行的自动执行程序。这些程序在设计上是不可变的,这防止了在合约部署后对业务逻辑进行任何更新。

虽然不可变性对于智能合约的去信任化、去中心化和安全性是必要的,但在某些情况下它可能是一个缺点。例如,不可变的代码可能使开发者无法修复存在漏洞的合约。

然而,随着对改进智能合约的研究不断深入,引入了几种升级模式。这些升级模式使开发者能够通过将业务逻辑放置在不同的合约中来升级智能合约(同时保持不可变性)。

先决条件

你应该对智能合约智能合约剖析以及以太坊虚拟机 (EVM)有很好的理解。本指南还假设读者已经掌握了智能合约编程。

什么是智能合约升级?

智能合约升级涉及在保留合约状态的同时更改智能合约的业务逻辑。需要澄清的是,可升级性和可变性并不相同,尤其是在智能合约的背景下。

你仍然无法更改部署在以太坊网络地址上的程序。但是,你可以更改用户与智能合约交互时执行的代码。

这可以通过以下方法完成:

  1. 创建智能合约的多个版本,并将状态(即数据)从旧合约迁移到合约的新实例。

  2. 创建单独的合约来存储业务逻辑和状态。

  3. 使用代理模式将函数调用从不可变的代理合约委托给可修改的逻辑合约。

  4. 创建一个不可变的主合约,该合约与灵活的卫星合约交互并依赖它们来执行特定函数。

  5. 使用钻石模式将函数调用从代理合约委托给逻辑合约。

升级机制 #1:合约迁移

合约迁移基于版本控制——即创建和管理同一软件的独特状态的想法。合约迁移涉及部署现有智能合约的新实例,并将存储和余额转移到新合约。

新部署的合约将具有空存储,允许你从旧合约中恢复数据并将其写入新的实现中。之后,你需要更新所有与旧合约交互的合约,以反映新地址。

合约迁移的最后一步是说服用户切换到使用新合约。新合约版本将保留用户余额和地址,从而保持不可变性。如果它是基于代币的合约,你还需要联系交易所废弃旧合约并使用新合约。

合约迁移是一种相对直接且安全的措施,用于在不破坏用户交互的情况下升级智能合约。然而,手动将用户存储和余额迁移到新合约非常耗时,并且可能产生高昂的 Gas 成本。

更多关于合约迁移的信息。 (opens in a new tab)

升级机制 #2:数据分离

升级智能合约的另一种方法是将业务逻辑和数据存储分离到单独的合约中。这意味着用户与逻辑合约交互,而数据存储在存储合约中。

逻辑合约包含用户与应用程序交互时执行的代码。它还保存存储合约的地址,并与其交互以获取和设置数据。

同时,存储合约保存与智能合约相关的状态,例如用户余额和地址。请注意,存储合约归逻辑合约所有,并在部署时配置了后者的地址。这可以防止未经授权的合约调用存储合约或更新其数据。

默认情况下,存储合约是不可变的——但你可以用新的实现替换它所指向的逻辑合约。这将更改在 EVM 中运行的代码,同时保持存储和余额完好无损。

使用此升级方法需要更新存储合约中的逻辑合约地址。出于前面解释的原因,你还必须使用存储合约的地址配置新的逻辑合约。

与合约迁移相比,数据分离模式可以说更容易实现。然而,你必须管理多个合约并实施复杂的授权方案,以保护智能合约免受恶意升级。

升级机制 #3:代理模式

代理模式也使用数据分离将业务逻辑和数据保存在单独的合约中。然而,在代理模式中,存储合约(称为代理)在代码执行期间调用逻辑合约。这与数据分离方法相反,在数据分离方法中,逻辑合约调用存储合约。

以下是代理模式中发生的情况:

  1. 用户与代理合约交互,代理合约存储数据,但不包含业务逻辑。

  2. 代理合约存储逻辑合约的地址,并使用 delegatecall 函数将所有函数调用委托给逻辑合约(包含业务逻辑)。

  3. 调用转发到逻辑合约后,将检索逻辑合约返回的数据并将其返回给用户。

使用代理模式需要了解 delegatecall 函数。基本上,delegatecall 是一个操作码,允许一个合约调用另一个合约,而实际的代码执行发生在调用合约的上下文中。在代理模式中使用 delegatecall 的一个含义是,代理合约读取和写入其存储,并执行存储在逻辑合约中的逻辑,就像调用内部函数一样。

摘自 Solidity 文档 (opens in a new tab)

存在一种特殊的消息调用变体,名为 delegatecall,它与消息调用相同,不同之处在于目标地址的代码在调用合约的上下文(即地址)中执行,并且 msg.sendermsg.value 不会改变它们的值。 这意味着合约可以在运行时从不同地址动态加载代码。存储、当前地址和余额仍然引用调用合约,只有代码取自被调用地址。

代理合约知道在用户调用函数时调用 delegatecall,因为它内置了一个 fallback 函数。在 Solidity 编程中,当函数调用与合约中指定的函数不匹配时,将执行回退函数 (opens in a new tab)

要使代理模式工作,需要编写一个自定义回退函数,指定代理合约应如何处理它不支持的函数调用。在这种情况下,代理的回退函数被编程为启动 delegatecall 并将用户的请求重新路由到当前的逻辑合约实现。

代理合约默认是不可变的,但可以创建具有更新业务逻辑的新逻辑合约。然后,执行升级只需更改代理合约中引用的逻辑合约的地址即可。

通过将代理合约指向新的逻辑合约,用户调用代理合约函数时执行的代码就会改变。这使我们能够升级合约的逻辑,而无需让用户与新合约交互。

代理模式是升级智能合约的流行方法,因为它们消除了与合约迁移相关的困难。然而,代理模式使用起来更复杂,如果使用不当,可能会引入严重的缺陷,例如函数选择器冲突 (opens in a new tab)

更多关于代理模式的信息 (opens in a new tab)

升级机制 #4:策略模式

这种技术受到策略模式 (opens in a new tab)的影响,该模式鼓励创建与其他程序交互以实现特定功能的软件程序。将策略模式应用于以太坊开发意味着构建一个调用其他合约函数的智能合约。

在这种情况下,主合约包含核心业务逻辑,但与其他智能合约(“卫星合约”)交互以执行某些函数。该主合约还存储每个卫星合约的地址,并可以在卫星合约的不同实现之间切换。

你可以构建一个新的卫星合约,并使用新地址配置主合约。这允许你更改智能合约的_策略_(即实现新逻辑)。

虽然与前面讨论的代理模式相似,但策略模式有所不同,因为用户交互的主合约包含业务逻辑。使用这种模式使你有机会对智能合约进行有限的更改,而不会影响核心基础设施。

主要缺点是这种模式主要用于推出次要升级。此外,如果主合约受到破坏(例如,通过黑客攻击),你将无法使用此升级方法。

升级机制 #5:钻石模式

钻石模式可以被认为是代理模式的改进。钻石模式与代理模式不同,因为钻石代理合约可以将函数调用委托给多个逻辑合约。

钻石模式中的逻辑合约被称为_切面 (facets)_。要使钻石模式工作,你需要在代理合约中创建一个映射,将函数选择器 (opens in a new tab)映射到不同的切面地址。

当用户进行函数调用时,代理合约会检查映射以找到负责执行该函数的切面。然后它调用 delegatecall(使用回退函数)并将调用重定向到适当的逻辑合约。

钻石升级模式相对于传统的代理升级模式有一些优势:

  1. 它允许你升级合约的一小部分,而无需更改所有代码。使用代理模式进行升级需要创建一个全新的逻辑合约,即使是次要升级也是如此。

  2. 所有智能合约(包括代理模式中使用的逻辑合约)都有 24KB 的大小限制,这可能是一个限制——特别是对于需要更多功能的复杂合约。钻石模式通过将函数拆分到多个逻辑合约中,轻松解决了这个问题。

  3. 代理模式对访问控制采用包罗万象的方法。有权访问升级功能的实体可以更改_整个_合约。但钻石模式支持模块化权限方法,你可以限制实体仅升级智能合约中的某些功能。

更多关于钻石模式的信息 (opens in a new tab)

升级智能合约的优缺点

优点缺点
智能合约升级可以更容易地修复在部署后阶段发现的漏洞。升级智能合约否定了代码不可变性的理念,这对去中心化和安全性有影响。
开发者可以使用逻辑升级为去中心化应用 (dapp) 添加新功能。用户必须信任开发者不会随意修改智能合约。
智能合约升级可以提高最终用户的安全性,因为可以快速修复错误。将升级功能编程到智能合约中增加了另一层复杂性,并增加了出现严重缺陷的可能性。
合约升级为开发者提供了更多空间来尝试不同的功能,并随着时间的推移改进 dapp。升级智能合约的机会可能会鼓励开发者更快地启动项目,而不在开发阶段进行尽职调查。
智能合约中不安全的访问控制或中心化可能使恶意行为者更容易执行未经授权的升级。

升级智能合约的注意事项

  1. 使用安全的访问控制/授权机制来防止未经授权的智能合约升级,尤其是在使用代理模式、策略模式或数据分离时。一个例子是限制对升级函数的访问,使得只有合约的所有者才能调用它。

  2. 升级智能合约是一项复杂的活动,需要高度的尽职调查以防止引入漏洞。

  3. 通过去中心化实施升级的过程来减少信任假设。可能的策略包括使用多重签名钱包合约来控制升级,或要求 DAO 成员投票批准升级。

  4. 注意升级合约所涉及的成本。例如,在合约迁移期间将状态(例如,用户余额)从旧合约复制到新合约可能需要不止一笔交易,这意味着更多的 Gas 费。

  5. 考虑实施时间锁 (timelocks) 来保护用户。时间锁是指对系统更改强制执行的延迟。时间锁可以与多重签名治理系统结合使用来控制升级:如果提议的操作达到所需的批准阈值,它不会执行,直到预定义的延迟时间过去。

时间锁让用户在不同意提议的更改(例如,逻辑升级或新的费用方案)时有一些时间退出系统。如果没有时间锁,用户需要信任开发者不会在没有事先通知的情况下在智能合约中实施任意更改。这里的缺点是时间锁限制了快速修补漏洞的能力。

资源

欧本齐柏林升级插件 (OpenZeppelin Upgrades Plugins) - 一套用于部署和保护可升级智能合约的工具。

教程

延伸阅读