智能合约设计模式是一种在区块链领域中用于编写智能合约的经验总结和最佳实践。类似于软件工程中的设计模式,智能合约设计模式提供了一套可重用的解决方案,用于解决智能合约开发中常见的问题和挑战。这些设计模式可以帮助开发者提高合约的安全性、可维护性和可扩展性。
异同点:
对于每一种模式,我们应当从一个简单的问题开始。
这是为何?
为何要创建这种模式?它是为了解决哪个问题而存在的?
对于“代理”模式,为何它与智能合约的不可变性有关?智能合约一旦部署,就无法对其业务逻辑进行任何更新。这引发了一个明显的问题。
一开始,这个问题通过“合约迁移”来解决。新版本的合约会被部署,而所有的状态和余额则需要转移到这个新实例。
然而,这种方法存在一个明显的缺点,即新的部署会导致合约的新地址。对于与更广泛生态系统集成的应用程序来说,这将要求所有第三方同样更新其代码库,以便指向新合约。
另一个缺点是将状态和余额转移到新实例的操作的复杂性。这不仅在 Gas 方面非常昂贵,而且还是一项非常敏感的操作。如果不正确地更新新合约的状态,可能会破坏其功能并导致安全漏洞。
显然,我们需要一种更简单的解决方案。我们如何在不改变合约地址的情况下更新合约的基本逻辑?我们如何将操作开销降至最低?
从这些问题中,出现了“代理模式”。
控制器合约(Controller Contract):控制器合约专注于业务逻辑的处理和对外提供服务接口。它通过访问数据合约来获取数据,并对数据进行逻辑处理,然后将结果写回数据合约。控制器合约可以根据不同的处理逻辑进行分类,例如命名空间控制器合约、代理控制器合约、业务控制器合约、工厂控制器合约等。通常情况下,控制器合约不存储任何数据,而是完全依赖外部输入来决定对数据合约的访问。有时,控制器合约可能会存储某个特定数据合约的地址或命名空间(通过命名空间在运行时获取合约地址)。
// 控制器合约
contract NamespaceController {
DataContract private dataContract;
constructor(address _dataContract) {
dataContract = DataContract(_dataContract);
}
// 通过控制器合约访问数据合约来获取数据并进行逻辑处理
function processData(uint256 input) external {
uint256 data = dataContract.getData();
// 对数据进行逻辑处理
uint256 result = data + input;
// 将处理结果写回数据合约
dataContract.setData(result);
}
}
数据合约(Data Contract):数据合约专注于定义数据结构和提供读写数据的接口。它定义了数据的存储方式和访问权限控制。为了实现数据的统一访问管理和权限控制,最好只将数据的读写接口暴露给相应的控制器合约,禁止其他方式的读写访问。
// 数据合约
contract DataContract {
uint256 private data;
// 仅允许控制器合约访问写操作
function setData(uint256 _data) external {
require(msg.sender == address(controller), "Access denied");
data = _data;
}
// 任何合约和用户都可以访问读操作
function getData() external view returns (uint256) {
return data;
}
}
// 主合约
contract MainContract {
DataContract public dataContract;
NamespaceController public namespaceController;
constructor() {
// 创建数据合约实例
dataContract = new DataContract();
// 创建命名空间控制器合约实例,并传入数据合约地址
namespaceController = new NamespaceController(address(dataContract));
}
// 获取数据合约地址
function getDataContractAddress() external view returns (address) {
return address(dataContract);
}
// 获取命名空间控制器合约地址
function getNamespaceControllerAddress() external view returns (address) {
return address(namespaceController);
}
}
基于CD模式,你可以按照自上而下的方式进行合约架构设计。首先从对外提供的服务接口开始设计各种控制器合约,然后逐步过渡到所需的数据模型和存储方式,最终设计各种数据合约。这种方法可以帮助你快速完成合约架构的设计,并确保业务逻辑与数据的有效分离。
透明代理的核心思想是为管理员用户和非管理员用户提供 2 条不同的执行路径。
如果管理员调用合约“代理”,函数将可用。对于其他任何人,所有调用都将通过回退函数委托给“实现”,即使存在匹配的函数签名。
这消除了歧义,管理员可以与“代理”函数交互,非管理员只能与“实现”函数交互。
pragma solidity ^0.8.0;
contract Proxy {
address public admin;
address public implementation;
constructor(address _implementation) {
admin = msg.sender;
implementation = _implementation;
}
fallback() external {
require(msg.sender == admin, "Only admin can call this function");
(bool success, ) = implementation.delegatecall(msg.data);
require(success, "Delegatecall to implementation failed");
}
}
contract Implementation {
// 合约的具体实现逻辑
function doSomething() external {
// 可以被管理员和非管理员调用
}
function doAnotherThing() external {
// 只能被管理员调用
require(msg.sender == address(proxy), "Only admin can call this function");
// 执行特定于管理员的逻辑
}
}
现在让我们来看一下透明代理和 2 个执行路径(管理员和非管理员)的 OpenZepplin 实现,以更好地理解发生了什么。
我们将从用户(非管理员)执行路径开始。
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
contract MyContractImplementation {
// 合约的具体实现逻辑
function doSomething() external {
// 可以被管理员和非管理员调用
}
function doAnotherThing() external {
// 只能被管理员调用
require(msg.sender == address(proxy), "Only admin can call this function");
// 执行特定于管理员的逻辑
}
}
contract MyContractProxy is TransparentUpgradeableProxy {
constructor(address _logic, address _admin, bytes memory _data) TransparentUpgradeableProxy(_logic, _admin, _data) {}
}
管理员流程引入了一个新的合约“代理管理员”和库 ERC1967Utils。下面你将看到它们是如何被使用的。
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/proxy/ProxyAdmin.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/proxy/utils/ERC1967Upgrade.sol";
import "@openzeppelin/contracts/proxy/utils/ERC1967Utils.sol";
contract MyContractImplementation {
// 合约的具体实现逻辑
function doSomething() external {
// 可以被管理员和非管理员调用
}
function doAnotherThing() external {
// 只能被管理员调用
require(msg.sender == address(proxy), "Only admin can call this function");
// 执行特定于管理员的逻辑
}
}
contract MyContractProxy is TransparentUpgradeableProxy {
constructor(address _logic, address _admin, bytes memory _data) TransparentUpgradeableProxy(_logic, _admin, _data) {}
}
contract MyProxyAdmin is ProxyAdmin {}
contract MyProxyAdminUpgradeable is ERC1967Upgrade, ProxyAdmin {}
contract MyContractProxyAdmin {
address public admin;
ProxyAdmin public proxyAdmin;
constructor(address _admin, address _proxyAdmin) {
admin = _admin;
proxyAdmin = ProxyAdmin(_proxyAdmin);
}
function upgradeAndCall(
MyContractProxy proxy,
address newImplementation,
bytes memory data
) external {
require(msg.sender == admin, "Only admin can call this function");
bytes memory callData = abi.encodeWithSignature("upgradeToAndCall(address,bytes)", newImplementation, data);
proxyAdmin.upgrade(proxy, newImplementation, callData);
}
}
contract MyContract {
// 代理合约
MyContractProxy public proxy;
constructor(address _implementation, address _admin) {
proxy = new MyContractProxy(_implementation, _admin, "");
}
function upgradeAndCall(
address newImplementation,
bytes memory data
) public {
MyContractProxyAdmin adminProxy = MyContractProxyAdmin(address(proxyAdmin()));
adminProxy.upgradeAndCall(proxy, newImplementation, data);
}
function proxyAdmin() internal view returns (address) {
return Address.functionDelegateCall(address(proxy), abi.encodeWithSignature("_proxyAdmin()"));
}
}