智能合约代理

admin
admin 2月22日
  • 在其它设备中阅读本文章

智能合约代理(Proxy)

引言

在区块链开发中,智能合约一旦部署到链上就无法更改,这种不可变性虽然提供了安全保障,但也带来了灵活性的挑战。代理模式应运而生,成为解决这一问题的关键技术。

基础概念

什么是代理模式?

代理(Proxy)本身并不是固有可升级的,但它是几乎所有可升级合约模式的基础。对代理合约的调用会通过 delegatecall 转发到实现合约(也称为逻辑合约)。这种机制允许合约逻辑与存储分离,从而实现合约的可升级性。

关键术语

  • 代理合约(Proxy Contract):用户直接交互的合约,负责转发调用并存储数据
  • 实现合约 / 逻辑合约(Implementation/Logic Contract):包含实际业务逻辑的合约
  • delegatecall:特殊的调用方式,在调用者的上下文中执行被调用合约的代码
  • 存储槽(Storage Slot):合约存储中的特定位置,用于存储变量

基本代理模式

EIP-1167 最小代理

EIP-1167 标准于 2018 年 6 月创建,旨在标准化以简单、便宜且不可变的方式克隆合约功能。该标准包含了针对代理合约优化的最小字节码重定向实现。通常与工厂模式一起使用。

// 最小代理合约示例
bytes memory bytecode = hex"363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3";

// 其中 "bebebebe..." 需要替换为实际的实现合约地址
bytecode = abi.encodePacked(
    bytecode[:20],
    implementationAddress,
    bytecode[40:]
);

代码解读

特点:

  • 实现地址 :在代理合约中是不可变的
  • 升级逻辑 :纯代理合约中没有可升级性
  • 合约验证 :可与 Etherscan 和其他区块浏览器配合使用。

优点:

  • 部署成本低

缺点:

  • 每次调用会增加一次 delegatecall 的费用
  • 无法升级实现合约

应用场景:

  • 当需要部署多个代码基本相同的合约时非常有用

实际应用:

已知漏洞:

  • 实现合约中不允许使用 delegatecallselfdestruct

可初始化代理

初始化的挑战

使用代理的主要好处之一是只需部署一次实现合约,然后可以部署多个指向它的代理合约。然而,缺点是无法在创建新代理时使用已经部署的实现合约中的构造函数。

正如开发者常问的:" 但我们该如何在没有 constructor() 的情况下工作?"

初始化函数

解决方案是使用 initialize() 函数来设置初始存储值:

uint8 private _initialized;

function initialize() external {
    require(msg.sender == owner);
    require(_initialized < 1);
    _initialized = 1;
    
    // 设置一些状态变量
    // 做初始化工作
}

优点:

  • 允许在新代理部署时设置初始存储

缺点:

  • 易受与初始化相关的攻击,尤其是未初始化的代理

应用场景:

  • 任何需要在代理合约部署时设置存储的代理,适用于大多数代理类型,包括 TPP 和 UUPS。

可升级代理

基本可升级代理

可升级代理类似于简单代理,只不过实现合约地址是可设置的,并保存在代理合约中。代理合约还包含授权的升级功能。

特点:

  • 实现地址 :位于代理存储中
  • 升级逻辑 :位于代理合约中
  • 合约验证 :根据具体实现,可能无法与像和 Etherscan 这样的区块浏览器配合使用

优点:

  • 通过使用代理降低部署成本
  • 实现合约可以升级

缺点:

  • 易受存储和功能冲突的影响
  • 安全性不如当前同类产品。
  • 每次调用都要承担来自代理的 delegatecall 费用

应用场景:

  • 这种基本风格已不再广泛使用。

已知漏洞:

  • 实现中不允许使用 delegatecallselfdestruct
  • 未初始化的代理
  • 存储冲突
  • 功能冲突

EIP-1967 可升级代理

这类似于基本可升级代理,但通过使用无结构存储模式减少存储冲突的风险。它不将实现合约地址存储在槽 0 或任何其他标准存储槽中。

相反,地址存储在预先商定的槽中。例如,OpenZeppelin 合约使用字符串 "eip1967.proxy.implementation" 的 keccak256 哈希值减去 1。由于这个槽广泛使用,区块浏览器可以识别并处理代理的使用。

减去 1 提供了额外的安全性,因为没有它,存储槽有一个已知的预映像,但在减去 1 之后,预映像是未知的。对于已知的预映像,存储槽可能会通过映射被覆盖,例如,其中存储槽的键是通过 keccak256 哈希确定的。

EIP-1967 还指定了一个用于管理员存储(auth)的槽,以及 Beacon 代理模式。

特点:

  • 实现地址 :位于代理合约中的唯一存储槽
  • 升级逻辑 :根据实现而有所不同

高级代理模式

透明代理模式(Transparent Proxy Pattern, TPP)

透明代理模式是一种解决功能选择器冲突的方法。在这种模式中,代理合约会检查调用者的地址。如果调用者是管理员,则调用代理的管理函数;如果调用者是其他地址,则将调用转发到实现合约。

modifier ifAdmin() {
    if (msg.sender == _getAdmin()) {
        _;
    } else {
        _fallback(); // redirects call to proxy
    }
}

优点:

  • 解决了功能选择器冲突问题
  • 安全性较高

缺点:

  • 每次调用都需要额外的 gas 来检查调用者是否是管理员
  • 管理员无法直接调用实现合约的函数

应用场景:

  • 需要高安全性的可升级合约

实际应用:

  • 许多 DeFi 项目,如 USDC

UUPS(Universal Upgradeable Proxy Standard)

UUPS 是一种将升级逻辑放在实现合约中而不是代理合约中的模式。这样,代理合约可以更加轻量级,并且不需要检查调用者是否是管理员。

// 实现合约中的升级函数
function upgradeToAndCall(address newImplementation, bytes memory data) internal {
    ...
}

优点:

  • 代理合约更加轻量级
  • 每次调用的 gas 成本更低

缺点:

  • 实现合约必须包含升级逻辑
  • 如果忘记在新实现中包含升级逻辑,可能会导致合约无法再次升级

应用场景:

  • 需要优化 gas 成本的可升级合约

实际应用:

  • OpenZeppelin 的 UUPSUpgradeable
  • 越来越多的新项目

Beacon 代理

Beacon 代理是一种允许同时升级多个代理合约的模式。在这种模式中,所有代理合约都指向一个 Beacon 合约,而 Beacon 合约则指向实现合约。当需要升级时,只需要更新 Beacon 合约中的实现地址,所有代理合约都会自动指向新的实现。

// Beacon 合约
contract UpgradeableBeacon {
    address private _implementation;
    address private _owner;
    
    function implementation() public view returns (address) {
        return _implementation;
    }
    
    function upgradeTo(address newImplementation) public {
        require(msg.sender == _owner, "Not authorized");
        _implementation = newImplementation;
    }
}

// Beacon 代理合约
contract BeaconProxy {
    address private _beacon;
    
    function _implementation() internal view returns (address) {
        return IBeacon(_beacon).implementation();
    }
    
    function _fallback() internal {
        _delegate(_implementation());
    }
}

优点:

  • 可以同时升级多个代理合约
  • 降低了升级的 gas 成本

缺点:

  • 增加了一层间接性
  • 每次调用都需要额外的 gas 来获取实现地址

应用场景:

  • 需要部署多个相同逻辑的可升级合约

实际应用:

  • OpenZeppelin 的 BeaconProxy
  • 一些 NFT 和 DeFi 项目

安全考虑

常见漏洞

  1. 存储冲突 :代理合约和实现合约使用相同的存储槽,可能导致数据覆盖
  2. 功能选择器冲突 :代理合约和实现合约有相同的函数选择器,可能导致调用错误的函数
  3. 未初始化的代理 :代理合约未正确初始化,可能被攻击者利用
  4. delegatecall 和 selfdestruct:实现合约中使用这些操作可能导致代理合约被销毁或逻辑被篡改
  5. 升级权限 :升级权限过于集中,可能导致单点故障

最佳实践

  1. 使用无结构存储 :避免存储冲突
  2. 使用透明代理或 UUPS:避免功能选择器冲突
  3. 确保正确初始化 :防止未初始化的代理漏洞
  4. 避免在实现中使用危险操作:不要在实现合约中使用delegatecallselfdestruct
  5. 实现多签或时间锁 :分散升级权限
  6. 全面测试 :在升级前进行全面的测试,确保新实现与旧实现兼容
  7. 使用成熟的库 :如 OpenZeppelin 的代理库

未来发展

新的代理模式

随着区块链技术的发展,新的代理模式不断涌现:

  • 钻石模式(Diamond Pattern):允许一个代理合约指向多个实现合约,每个实现合约负责不同的功能
  • 可组合代理 :允许代理合约组合多个实现合约的功能

EIP 和标准化

社区正在努力标准化代理模式,以提高互操作性和安全性:

  • EIP-1822:提出了 UUPS 标准
  • EIP-2535:提出了钻石模式标准

总结

代理模式是区块链开发中解决合约不可变性问题的关键技术。通过使用代理模式,开发者可以在保持合约地址不变的情况下升级合约逻辑,提高了区块链应用的灵活性和可维护性。

然而,代理模式也带来了新的安全挑战,开发者需要了解各种代理模式的优缺点,选择适合自己项目的模式,并遵循最佳实践来避免潜在的漏洞。

随着区块链技术的不断发展,代理模式也在不断演进,新的模式和标准将为开发者提供更多选择,使区块链应用更加强大和灵活。

参考资料

  1. OpenZeppelin Proxy Contracts
  2. EIP-1967: Standard Proxy Storage Slots
  3. EIP-1822: Universal Upgradeable Proxy Standard (UUPS)
  4. EIP-2535: Diamond Standard