前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >智能合约设计模式:讲解代理模式及其安全漏洞(已删除侵权内容版本)

智能合约设计模式:讲解代理模式及其安全漏洞(已删除侵权内容版本)

作者头像
苏泽
发布2024-03-28 09:37:21
1090
发布2024-03-28 09:37:21
举报
(经修改 已将部分侵权的图片全部删除) 我们首先来看看什么是设计模式 和我们软件工程里面的设计模式有什么异同?

智能合约设计模式是一种在区块链领域中用于编写智能合约的经验总结和最佳实践。类似于软件工程中的设计模式,智能合约设计模式提供了一套可重用的解决方案,用于解决智能合约开发中常见的问题和挑战。这些设计模式可以帮助开发者提高合约的安全性、可维护性和可扩展性。

异同点:

  • 相同点:智能合约设计模式和软件工程中的设计模式都是为了解决特定问题而提供的经验总结和最佳实践。它们都旨在提高代码的可读性、可维护性和可重用性。
  • 不同点:智能合约设计模式更加关注区块链特有的问题和挑战,例如安全性、可信任性和去中心化。智能合约设计模式还需要考虑智能合约的生命周期管理、鉴权和区块链网络的特性。而软件工程中的设计模式更加关注传统软件开发中的问题,例如模块化、封装和代码复用

总而言之,智能合约实现上要达到的目标是:完备的业务功能、精悍的代码逻辑、良好的模块抽象、清晰的合约结构、合理的安全检查、完备的升级方案

为何会出现这种模式存在的情况?

对于每一种模式,我们应当从一个简单的问题开始。

这是为何?

为何要创建这种模式?它是为了解决哪个问题而存在的?

对于“代理”模式,为何它与智能合约的不可变性有关?智能合约一旦部署,就无法对其业务逻辑进行任何更新。这引发了一个明显的问题。

我们如何升级智能合约?

一开始,这个问题通过“合约迁移”来解决。新版本的合约会被部署,而所有的状态和余额则需要转移到这个新实例。

然而,这种方法存在一个明显的缺点,即新的部署会导致合约的新地址。对于与更广泛生态系统集成的应用程序来说,这将要求所有第三方同样更新其代码库,以便指向新合约。

另一个缺点是将状态和余额转移到新实例的操作的复杂性。这不仅在 Gas 方面非常昂贵,而且还是一项非常敏感的操作。如果不正确地更新新合约的状态,可能会破坏其功能并导致安全漏洞。

显然,我们需要一种更简单的解决方案。我们如何在不改变合约地址的情况下更新合约的基本逻辑?我们如何将操作开销降至最低?

从这些问题中,出现了“代理模式”。

设计模式:CD

控制器合约(Controller Contract):控制器合约专注于业务逻辑的处理和对外提供服务接口。它通过访问数据合约来获取数据,并对数据进行逻辑处理,然后将结果写回数据合约。控制器合约可以根据不同的处理逻辑进行分类,例如命名空间控制器合约、代理控制器合约、业务控制器合约、工厂控制器合约等。通常情况下,控制器合约不存储任何数据,而是完全依赖外部输入来决定对数据合约的访问。有时,控制器合约可能会存储某个特定数据合约的地址或命名空间(通过命名空间在运行时获取合约地址)。

代码语言:javascript
复制
// 控制器合约
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):数据合约专注于定义数据结构和提供读写数据的接口。它定义了数据的存储方式和访问权限控制。为了实现数据的统一访问管理和权限控制,最好只将数据的读写接口暴露给相应的控制器合约,禁止其他方式的读写访问。

代码语言:javascript
复制
// 数据合约
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 条不同的执行路径。

如果管理员调用合约“代理”,函数将可用。对于其他任何人,所有调用都将通过回退函数委托给“实现”,即使存在匹配的函数签名。

这消除了歧义,管理员可以与“代理”函数交互,非管理员只能与“实现”函数交互。

代码语言:javascript
复制
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 实现,以更好地理解发生了什么。

我们将从用户(非管理员)执行路径开始。

用户访问(非管理员)
代码语言:javascript
复制
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) {}
}
  1. 用户调用"TransparentUpgradeableProxy"合约的函数。
  2. 由于没有与用户调用的函数匹配的函数签名,将触发fallback()函数。
  3. fallback()函数委托调用_fallback()函数。
  4. _fallback()函数在"TransparentUpgradeableProxy"合约中执行。
  5. 在_fallback()函数中,检查用户是否为管理员用户。
  6. 由于用户不是管理员用户,调用super._fallback()将调用父合约"ERC1967Proxy"的_fallback()函数。
  7. 在"ERC1967Proxy"合约的_fallback()函数中,调用_delegate(_implementation())。
  8. _delegate()函数使用内联汇编执行委托调用。
  9. 委托调用将控制权传递给实现合约,并返回结果。
  10. 委托调用的返回数据被复制到内存中,并作为返回结果返回给用户。
管理员访问

管理员流程引入了一个新的合约“代理管理员”和库 ERC1967Utils。下面你将看到它们是如何被使用的。

代码语言:javascript
复制
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()"));
    }
}
  1. 构造函数中设置了管理员_admin。管理员是一个代理管理员合约(ProxyAdmin)的地址。
  2. 管理员用户必须通过ProxyAdmin合约进行调用,而不是直接调用TransparentUpgradeableProxy合约。
  3. 管理员用户调用upgradeAndCall函数,传入目标代理合约、新实现地址和新实现的数据(可选)。
  4. upgradeAndCall函数将调用代理合约的upgradeToAndCall函数。
  5. 由于所有调用最终都会落到fallback函数,fallback函数将调用私有的_fallback函数。
  6. 在_fallback函数中,再次进行管理员检查,但这次是使用_proxyAdmin()函数获取管理员地址。
  7. 通过管理员检查后,验证调用的是特定的upgradeToAndCall方法,如果是,则调用_dispatchUpgradeToAndCall()函数。
  8. _dispatchUpgradeToAndCall函数从calldata中获取新实现地址,并使用ERC1967Utils.UpgradeToAndCall函数执行升级操作。
  9. ERC1967Utils.UpgradeToAndCall函数验证新实现地址的代码是否非零,并根据ERC-1967规范更新存储空间中的新实现地址。
  10. 如果数据长度大于0,表示用户希望在升级后执行一些调用操作,因此在新地址上执行委托调用。
  11. 如果数据长度为0,则验证调用没有附带ether,以防止资金被困在合约中。

概念已经变成了代码,你已经看到了理论如何在 solidity 中实现。这将帮助我们加深对代理工作原理和需要注意的潜在安全漏洞的理解

本文参与?腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-03-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 总而言之,智能合约实现上要达到的目标是:完备的业务功能、精悍的代码逻辑、良好的模块抽象、清晰的合约结构、合理的安全检查、完备的升级方案。
  • 为何会出现这种模式存在的情况?
  • 我们如何升级智能合约?
  • 设计模式:CD
  • 设计模式之 透明代理
    • 深入代码
      • 用户访问(非管理员)
        • 管理员访问
        • 概念已经变成了代码,你已经看到了理论如何在 solidity 中实现。这将帮助我们加深对代理工作原理和需要注意的潜在安全漏洞的理解。
        相关产品与服务
        访问管理
        访问管理(Cloud Access Management,CAM)可以帮助您安全、便捷地管理对腾讯云服务和资源的访问。您可以使用CAM创建子用户、用户组和角色,并通过策略控制其访问范围。CAM支持用户和角色SSO能力,您可以根据具体管理场景针对性设置企业内用户和腾讯云的互通能力。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
        http://www.vxiaotou.com