title: 48. 透明代理 tags: solidity proxy OpenZeppelin WTF Solidity极简入门: 48. 透明代理 我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。 推特:@0xAAScience|@WTFAcademy 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity 这一讲,我们将介绍代理合约的选择器冲突(Selector Clash),以及这一问题的解决方案:透明代理(Transparent Proxy)。
title: 48. 透明代理 tags: - solidity - proxy - OpenZeppelin
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
这一讲,我们将介绍代理合约的选择器冲突(Selector Clash),以及这一问题的解决方案:透明代理(Transparent Proxy)。教学代码由OpenZeppelin的TransparentUpgradeableProxy简化而成,不应用于生产。
智能合约中,函数选择器(selector)是函数签名的哈希的前4个字节。例如mint(address account)的选择器为bytes4(keccak256("mint(address)")),也就是0x6a627842。更多关于选择器的内容见WTF Solidity极简教程第29讲:函数选择器
由于函数选择器仅有4个字节,范围很小,因此两个不同的函数可能会有相同的选择器,例如下面两个函数:
// 选择器冲突的例子 contract Foo { function burn(uint256) external {} function collate_propagate_storage(bytes16) external {} }
https://www.aiknowledge.cn/images/WTF-Solidity/48-1.webp
示例中,函数burn()和collate_propagate_storage()的选择器都为0x42966c68,是一样的,这种情况被称为“选择器冲突”。在这种情况下,EVM无法通过函数选择器分辨用户调用哪个函数,因此该合约无法通过编译。
由于代理合约和逻辑合约是两个合约,就算他们之间存在“选择器冲突”也可以正常编译,这可能会导致很严重的安全事故。举个例子,如果逻辑合约的a函数和代理合约的升级函数的选择器相同,那么管理人就会在调用a函数的时候,将代理合约升级成一个黑洞合约,后果不堪设想。
目前,有两个可升级合约标准解决了这一问题:透明代理Transparent Proxy和通用可升级代理UUPS。
透明代理的逻辑非常简单:管理员可能会因为“函数选择器冲突”,在调用逻辑合约的函数时,误调用代理合约的可升级函数。那么限制管理员的权限,不让他调用任何逻辑合约的函数,就能解决冲突:
这里的代理合约和第47讲的非常相近,只是fallback()函数限制了管理员地址的调用。
它包含3个变量:
implementation:逻辑合约地址。admin:admin地址。words:字符串,可以通过逻辑合约的函数改变。它包含3个函数
fallback():回调函数,将调用委托给逻辑合约,不能由admin调用。upgrade():升级函数,改变逻辑合约地址,只能由admin调用。// 透明可升级合约的教学代码,不要用于生产。 contract TransparentProxy { address implementation; // logic合约地址 address admin; // 管理员 string public words; // 字符串,可以通过逻辑合约的函数改变 // 构造函数,初始化admin和逻辑合约地址 constructor(address _implementation){ admin = msg.sender; implementation = _implementation; } // fallback函数,将调用委托给逻辑合约 // 不能被admin调用,避免选择器冲突引发意外 fallback() external payable { require(msg.sender != admin); (bool success, bytes memory data) = implementation.delegatecall(msg.data); } // 升级函数,改变逻辑合约地址,只能由admin调用 function upgrade(address newImplementation) external { if (msg.sender != admin) revert(); implementation = newImplementation; } }
这里的新、旧逻辑合约与第47讲一样。逻辑合约包含3个状态变量,与代理合约保持一致,防止插槽冲突;包含一个函数foo(),旧逻辑合约会将words的值改为"old",新的会改为"new"。
// 旧逻辑合约 contract Logic1 { // 状态变量和proxy合约一致,防止插槽冲突 address public implementation; address public admin; string public words; // 字符串,可以通过逻辑合约的函数改变 // 改变proxy中状态变量,选择器: 0xc2985578 function foo() public{ words = "old"; } } // 新逻辑合约 contract Logic2 { // 状态变量和proxy合约一致,防止插槽冲突 address public implementation; address public admin; string public words; // 字符串,可以通过逻辑合约的函数改变 // 改变proxy中状态变量,选择器:0xc2985578 function foo() public{ words = "new"; } }
Remix实现部署新旧逻辑合约Logic1和Logic2。
https://www.aiknowledge.cn/images/WTF-Solidity/48-2.webp
https://www.aiknowledge.cn/images/WTF-Solidity/48-3.webp
部署透明代理合约TranparentProxy,将implementation地址指向把旧逻辑合约。
https://www.aiknowledge.cn/images/WTF-Solidity/48-4.webp
利用选择器0xc2985578,在代理合约中调用旧逻辑合约Logic1的foo()函数。调用将失败,因为管理员不能调用逻辑合约。
https://www.aiknowledge.cn/images/WTF-Solidity/48-5.webp
切换新钱包,利用选择器0xc2985578,在代理合约中调用旧逻辑合约Logic1的foo()函数,将words的值改为"old",调用将成功。
https://www.aiknowledge.cn/images/WTF-Solidity/48-6.webp
切换回管理员钱包,调用upgrade(),将implementation地址指向新逻辑合约Logic2。
https://www.aiknowledge.cn/images/WTF-Solidity/48-7.webp
切换新钱包,利用选择器0xc2985578,在代理合约中调用新逻辑合约Logic2的foo()函数,将words的值改为"new"。
https://www.aiknowledge.cn/images/WTF-Solidity/48-8.webp
这一讲,我们介绍了代理合约中的“选择器冲突”,以及如何利用透明代理避免这个问题。透明代理的逻辑简单,通过限制管理员调用逻辑合约解决“选择器冲突”问题。它也有缺点,每次用户调用函数时,都会多一步是否为管理员的检查,消耗更多gas。但瑕不掩瑜,透明代理仍是大多数项目方选择的方案。
下一讲,我们会介绍省gas但是也更加复杂的通用可升级代理UUPS。