苏泽
大家好 这里是苏泽 一个钟爱区块链技术的后端开发者
本篇专栏++←++ 持续记录本人自学两年走过无数弯路的智能合约学习笔记和经验总结 如果喜欢拜托三连支持~
我们首先来看看什么是设计模式 和我们软件工程里面的设计模式有什么异同?
智能合约设计模式是一种在区块链领域中用于编写智能合约的经验总结和最佳实践。类似于软件工程中的设计模式,智能合约设计模式提供了一套可重用的解决方案,用于解决智能合约开发中常见的问题和挑战。这些设计模式可以帮助开发者提高合约的安全性、可维护性和可扩展性。
异同点:
- 相同点:智能合约设计模式和软件工程中的设计模式都是为了解决特定问题而提供的经验总结和最佳实践。它们都旨在提高代码的可读性、可维护性和可重用性。
- 不同点:智能合约设计模式更加关注区块链特有的问题和挑战,例如安全性、可信任性和去中心化。智能合约设计模式还需要考虑智能合约的生命周期管理、鉴权和区块链网络的特性。而软件工程中的设计模式更加关注传统软件开发中的问题,例如模块化、封装和代码复用。
总而言之,智能合约实现上要达到的目标是:完备的业务功能、精悍的代码逻辑、良好的模块抽象、清晰的合约结构、合理的安全检查、完备的升级方案。
智能合约设计模式概述
2019年,IEEE收录了维也纳大学一篇题为《Design Patterns For Smart Contracts In the Ethereum Ecosystem》的论文。这篇论文分析了那些火热的Solidity开源项目,结合以往的研究成果,整理出了18种设计模式。这些设计模式涵盖了安全性、可维护性、生命周期管理、鉴权等多个方面。
历史
了解给定"智能合约设计模式"背后的历史非常有价值。这阐明了它为什么出现,它解决了什么具体问题以及沿途做出的设计权衡。
为什么存在这种模式?
对于每种模式,我们应该从一个简单的问题开始。
"为什么?"。
为什么创建这种模式,它解决了什么问题?
对于"代理"来说,为什么它来源于是智能合约不可变的。合约是不可变的,这阻止了合约部署后对业务逻辑的任何更新。这引发了一个明显的问题。
我们如何升级我们的智能合约?
这个问题最初是通过"合约迁移"来解决的。合约的新版本将被部署,并且所有状态和余额将需要被转移到这个新实例。
这种方法的一个明显缺点是,新部署会导致新的合约地址。对于集成到更广泛生态系统中的应用程序,这将要求所有第三方也更新其代码库以指向新合约。
另一个缺点是将状态和余额转移到这个新实例的操作复杂性。这不仅会在 Gas 方面非常昂贵,而且还将是一个非常敏感的操作。不正确地更新新合约的状态可能会破坏功能并导致安全漏洞。
显然需要一个更简单的解决方案。我们如何在不更改其地址的情况下更新合约的基础逻辑?我们如何最小化操作开销?
从这些问题中形成了"代理模式"。
设计模式:CD
-
控制器合约(Controller Contract):控制器合约专注于业务逻辑的处理和对外提供服务接口。它通过访问数据合约来获取数据,并对数据进行逻辑处理,然后将结果写回数据合约。控制器合约可以根据不同的处理逻辑进行分类,例如命名空间控制器合约、代理控制器合约、业务控制器合约、工厂控制器合约等。通常情况下,控制器合约不存储任何数据,而是完全依赖外部输入来决定对数据合约的访问。有时,控制器合约可能会存储某个特定数据合约的地址或命名空间(通过命名空间在运行时获取合约地址)。
-
数据合约(Data Contract):数据合约专注于定义数据结构和提供读写数据的接口。它定义了数据的存储方式和访问权限控制。为了实现数据的统一访问管理和权限控制,最好只将数据的读写接口暴露给相应的控制器合约,禁止其他方式的读写访问。
基于CD模式,你可以按照自上而下的方式进行合约架构设计。首先从对外提供的服务接口开始设计各种控制器合约,然后逐步过渡到所需的数据模型和存储方式,最终设计各种数据合约。这种方法可以帮助你快速完成合约架构的设计,并确保业务逻辑与数据的有效分离。
设计模式之 透明代理
透明代理的核心思想是为管理员用户和非管理员用户提供 2 条不同的执行路径。
如果管理员调用合约"代理",函数将可用。对于其他任何人,所有调用都将通过回退函数委托给"实现",即使存在匹配的函数签名。
这消除了歧义,管理员可以与"代理"函数交互,非管理员只能与"实现"函数交互。
深入代码
现在让我们来看一下透明代理和 2 个执行路径(管理员和非管理员)的 OpenZepplin 实现,以更好地理解发生了什么。
我们将从用户(非管理员)执行路径开始。
用户访问(非管理员)
- 用户调用"TransparentUpgradeableProxy"合约的函数。
- 由于没有与用户调用的函数匹配的函数签名,将触发fallback()函数。
- fallback()函数委托调用_fallback()函数。
- _fallback()函数在"TransparentUpgradeableProxy"合约中执行。
- 在_fallback()函数中,检查用户是否为管理员用户。
- 由于用户不是管理员用户,调用super._fallback()将调用父合约"ERC1967Proxy"的_fallback()函数。
- 在"ERC1967Proxy"合约的_fallback()函数中,调用_delegate(_implementation())。
- _delegate()函数使用内联汇编执行委托调用。
- 委托调用将控制权传递给实现合约,并返回结果。
- 委托调用的返回数据被复制到内存中,并作为返回结果返回给用户。
管理员访问
管理员流程引入了一个新的合约"代理管理员"和库 ERC1967Utils。下面你将看到它们是如何被使用的。
- 构造函数中设置了管理员_admin。管理员是一个代理管理员合约(ProxyAdmin)的地址。
- 管理员用户必须通过ProxyAdmin合约进行调用,而不是直接调用TransparentUpgradeableProxy合约。
- 管理员用户调用upgradeAndCall函数,传入目标代理合约、新实现地址和新实现的数据(可选)。
- upgradeAndCall函数将调用代理合约的upgradeToAndCall函数。
- 由于所有调用最终都会落到fallback函数,fallback函数将调用私有的_fallback函数。
- 在_fallback函数中,再次进行管理员检查,但这次是使用_proxyAdmin()函数获取管理员地址。
- 通过管理员检查后,验证调用的是特定的upgradeToAndCall方法,如果是,则调用_dispatchUpgradeToAndCall()函数。
- _dispatchUpgradeToAndCall函数从calldata中获取新实现地址,并使用ERC1967Utils.UpgradeToAndCall函数执行升级操作。
- ERC1967Utils.UpgradeToAndCall函数验证新实现地址的代码是否非零,并根据ERC-1967规范更新存储空间中的新实现地址。
- 如果数据长度大于0,表示用户希望在升级后执行一些调用操作,因此在新地址上执行委托调用。
- 如果数据长度为0,则验证调用没有附带ether,以防止资金被困在合约中。