智能合约代理
智能合约代理(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
的费用 - 无法升级实现合约
应用场景:
- 当需要部署多个代码基本相同的合约时非常有用
实际应用:
已知漏洞:
- 实现合约中不允许使用
delegatecall
和selfdestruct
可初始化代理
初始化的挑战
使用代理的主要好处之一是只需部署一次实现合约,然后可以部署多个指向它的代理合约。然而,缺点是无法在创建新代理时使用已经部署的实现合约中的构造函数。
正如开发者常问的:" 但我们该如何在没有 constructor()
的情况下工作?"
初始化函数
解决方案是使用 initialize()
函数来设置初始存储值:
uint8 private _initialized;
function initialize() external {
require(msg.sender == owner);
require(_initialized < 1);
_initialized = 1;
// 设置一些状态变量
// 做初始化工作
}
优点:
- 允许在新代理部署时设置初始存储
缺点:
- 易受与初始化相关的攻击,尤其是未初始化的代理
应用场景:
- 任何需要在代理合约部署时设置存储的代理,适用于大多数代理类型,包括 TPP 和 UUPS。
可升级代理
基本可升级代理
可升级代理类似于简单代理,只不过实现合约地址是可设置的,并保存在代理合约中。代理合约还包含授权的升级功能。
特点:
- 实现地址 :位于代理存储中
- 升级逻辑 :位于代理合约中
- 合约验证 :根据具体实现,可能无法与像和 Etherscan 这样的区块浏览器配合使用
优点:
- 通过使用代理降低部署成本
- 实现合约可以升级
缺点:
- 易受存储和功能冲突的影响
- 安全性不如当前同类产品。
- 每次调用都要承担来自代理的
delegatecall
费用
应用场景:
- 这种基本风格已不再广泛使用。
已知漏洞:
- 实现中不允许使用
delegatecall
和selfdestruct
- 未初始化的代理
- 存储冲突
- 功能冲突
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 项目
安全考虑
常见漏洞
- 存储冲突 :代理合约和实现合约使用相同的存储槽,可能导致数据覆盖
- 功能选择器冲突 :代理合约和实现合约有相同的函数选择器,可能导致调用错误的函数
- 未初始化的代理 :代理合约未正确初始化,可能被攻击者利用
- delegatecall 和 selfdestruct:实现合约中使用这些操作可能导致代理合约被销毁或逻辑被篡改
- 升级权限 :升级权限过于集中,可能导致单点故障
最佳实践
- 使用无结构存储 :避免存储冲突
- 使用透明代理或 UUPS:避免功能选择器冲突
- 确保正确初始化 :防止未初始化的代理漏洞
- 避免在实现中使用危险操作:不要在实现合约中使用
delegatecall
和selfdestruct
- 实现多签或时间锁 :分散升级权限
- 全面测试 :在升级前进行全面的测试,确保新实现与旧实现兼容
- 使用成熟的库 :如 OpenZeppelin 的代理库
未来发展
新的代理模式
随着区块链技术的发展,新的代理模式不断涌现:
- 钻石模式(Diamond Pattern):允许一个代理合约指向多个实现合约,每个实现合约负责不同的功能
- 可组合代理 :允许代理合约组合多个实现合约的功能
EIP 和标准化
社区正在努力标准化代理模式,以提高互操作性和安全性:
- EIP-1822:提出了 UUPS 标准
- EIP-2535:提出了钻石模式标准
总结
代理模式是区块链开发中解决合约不可变性问题的关键技术。通过使用代理模式,开发者可以在保持合约地址不变的情况下升级合约逻辑,提高了区块链应用的灵活性和可维护性。
然而,代理模式也带来了新的安全挑战,开发者需要了解各种代理模式的优缺点,选择适合自己项目的模式,并遵循最佳实践来避免潜在的漏洞。
随着区块链技术的不断发展,代理模式也在不断演进,新的模式和标准将为开发者提供更多选择,使区块链应用更加强大和灵活。