深入探讨代理(Proxy)
- 原文链接:proxies.yacademy.dev/pages...
- 译者:AI翻译官,校对:翻译小组
- 本文链接:learnblockchain.cn/article…
代理
Vitalik 说:“要有代理(Let there be Proxies)!”
代理(Proxy)本身并不是固有可升级的,但代理是几乎所有可升级代理模式的基础。对代理合约的调用会通过 delegatecall
转发到实现合约。实现合约也称为逻辑合约。
在一些变体中,只有当调用者匹配“所有者”地址时,调用才会被转发到代理。
实现地址 - 在代理合约中是不可变的。
升级逻辑 - 纯代理合约中没有可升级性。
合约验证 - 可与 Etherscan ( 示例 )和其他区块浏览器配合使用。
用例
- 当需要部署多个代码基本相同的合约时非常有用。
优点
- 部署成本低。
缺点
- 每次调用会增加一次
delegatecall
的费用。
示例
- Uniswap V1 AMM 池
- Synthetix
已知漏洞
- 实现中不允许使用 delegatecall 和 selfdestruct
变体
- EIP-1167 标准于 2018 年 6 月创建,旨在标准化以简单、便宜且不可变的方式克隆合约功能。该标准包含了针对代理合约优化的最小字节码重定向实现。通常与工厂模式一起使用。
深入阅读
- 野外的最小代理
- OpenZeppelin 核心代理合约
- 深入研究最小代理合约
可初始化代理
“但我们该如何在没有
constructor()
的情况下工作?”
现代的代理大多数都是可初始化的(initializeable)。使用代理的主要好处之一是只需部署一次实现合约(即逻辑合约),然后可以部署多个指向它的代理合约。然而,缺点是你无法在创建新代理时使用已经部署的实现合约中的构造函数。
相反,使用 initialize()
函数来设置初始存储值:
uint8 private _initialized;
function initializer() external {
require(msg.sender == owner);
require(_initialized < 1);
_initialized = 1;
// 设置一些状态变量
// 做初始化工作
}
用例
- 任何需要在代理合约部署时设置存储的代理。
优点
- 允许在新代理部署时设置初始存储。
缺点
- 易受与初始化相关的攻击,尤其是未初始化的代理。
示例
- 此功能与大多数现代代理类型(包括 TPP 和 UUPS)一起使用,除非没有需要在代理部署时设置存储的用例。
已知漏洞
- 未初始化的代理
变体
- 克隆工厂合约模型 - 在创建事务中使用克隆初始化。
- 具有不可变参数的克隆 - 允许创建具有不可变参数的克隆合约,这些参数存储在代理合约的代码区域。当调用时,参数附加到
delegatecall
的 calldata 中,实现合约函数随后从 calldata 中读取参数。此模式可以消除使用初始化器的需要,但缺点是当前该合约无法在 Etherscan 上验证 。
深入阅读
- 可初始化 - OZ
可升级代理
人们说:“但我们想升级我们的不可变合约!”
可升级代理类似于代理 ,只不过实现合约地址是可设置的,并保存在代理合约中。代理合约还包含授权的升级功能。第一个可升级代理合约之一由 Nick Johnson 在 2016 年编写。
出于安全考虑,还建议使用某种形式的访问控制,以区分拥有者/调用者和拥有权限升级合约的管理员。
实现地址 - 位于代理存储中。
升级逻辑 - 位于代理合约中。
合约验证 - 根据具体实现,可能无法与像 Etherscan 这样的区块浏览器配合使用。
用例
- 一个简约的升级合约。适用于学习项目。
优点
- 通过使用代理降低部署成本。
- 实现合约可以升级。
缺点
- 易受存储和功能冲突的影响。
- 安全性低于现代对手。
- 每次调用都要承担来自代理的
delegatecall
费用。
示例
- 这种基本风格现在不再广泛使用。
已知漏洞
- 实现中不允许使用 delegatecall 和 selfdestruct
- 未初始化的代理
- 存储冲突
- 功能冲突
深入阅读
- 第一个代理合约
- 编写可升级合约
EIP-1967 可升级代理
解决存储冲突的“解决方案”
这类似于 可升级代理 ,但通过使用无结构存储模式减少存储冲突的风险。它不将实现合约地址存储在槽 0 或任何其他标准存储槽中。
相反,地址存储在预先商定的槽中。例如,OpenZeppelin 合约使用字符串“eip1967.proxy.implementation”的 keccak-256 哈希值减去 1。由于这个槽广泛使用,区块浏览器可以识别并处理代理的使用。
*减去 1 提供了额外的安全性,因为没有它,存储槽有一个已知的预映像,但在减去 1 之后,预映像是未知的。对于已知的预映像,存储槽可能会通过映射被覆盖,例如,其中存储槽的键是通过 keccak-256 哈希确定的。
EIP-1967 还指定了一个用于管理员存储(auth)的槽,以及将要详细讨论的 Beacon 代理。
实现地址 - 位于代理合约中的唯一存储槽。
升级逻辑 - 根据实现而有所不同。
合约验证 - 是的,大多数 EVM 区块浏览器支持它。
用例
- 当你需要比基本的 可升级代理 更高的安全性时。
优点
- 降低存储冲突的风险。
- 区块浏览器兼容性
缺点
- 易受函数冲突的影响。
- 不如现代对手安全。
- 每次调用都会产生来自 代理 的
delegatecall
成本。
示例
- 尽管 EIP-1967 存储槽模式已在大多数现代可升级代理类型中广泛采用,但这种简单的合约在实际中并不像一些较新的模式如 TPP、UUPS 和 Beacon 那样常见。
已知漏洞
- 实现中不允许 delegatecall 和 selfdestruct
- 未初始化的代理
- 函数冲突
进一步阅读
-
EIP-1967 标准代理存储槽
-
代理委托
-
- *
透明代理 (TPP)
解决函数冲突的“解决方案”
这与 可升级代理 类似,通常结合 EIP-1967。但是,如果调用者是代理的管理员,代理将不会委托任何调用;如果调用者是其他地址,代理将始终委托调用,即使函数签名与代理自己的某个函数匹配。这通常通过像 OpenZeppelin 中的修饰符实现:
modifier ifAdmin() {
if (msg.sender == _getAdmin()) {
_;
} else {
_fallback(); // 将调用重定向到代理
}
}
以及在 fallback()
中的检查:
require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target");
实现地址 - 位于代理合约中的唯一存储槽 (EIP-1967)。
升级逻辑 - 位于代理合约中,使用 修饰符 来重新路由非管理员调用者。
合约验证 - 是的,大多数 EVM 区块浏览器支持它。
用例
- 这种模式因其可升级性和对某些函数和存储冲突漏洞的保护而被广泛使用。
优点
- 消除了管理员函数冲突的可能性,因为他们从未被重定向到实现合约。
- 由于升级逻辑位于代理上,如果代理处于未初始化状态或实现合约被自毁,则仍然可以将实现设置为新地址。
- 降低了由于使用 EIP-1967 存储槽而导致的存储冲突风险。
- 区块浏览器兼容性。
缺点
- 每次调用不仅会产生来自 代理 的
delegatecall
的运行时 gas 成本,还会产生检查调用者是否为管理员的 SLOAD 成本。 - 由于升级逻辑位于代理上,因此字节码更多,部署成本更高。
示例
- dYdX
- USDC
- Aztec
- Github 上的数百个项目
已知漏洞
- 实现中不允许 delegatecall 和 selfdestruct
- 未初始化的代理
- 存储冲突
进一步阅读
-
透明代理模式
-
- *
通用可升级代理标准 (UUPS)
如果我们将升级逻辑移到实现合约呢?🤔
EIP-1822 描述了一种可升级代理模式的标准,其中 upgrade
逻辑存储在实现合约中。这样,就不需要在代理级别检查调用者是否为管理员,从而节省 gas。它还消除了实现合约上的函数与代理中的升级逻辑冲突的可能性。
UUPS 的缺点是它被认为比 TPP 更具风险。如果代理没有正确初始化,或者实现合约被自毁,则无法保存代理,因为升级逻辑位于实现合约上。
UUPS 代理在升级时还包含一个额外的检查,以确保新的实现合约是可升级的。
该代理合约通常结合 EIP-1967。
实现地址 - 位于代理合约中的唯一存储槽 (EIP-1967)。
升级逻辑 - 位于实现合约中。
合约验证 - 是的,大多数 EVM 区块浏览器支持它。
用例
- 目前这是协议在部署可升级合约时最广泛使用的模式。
优点
- 消除了实现合约上的函数与代理合约冲突的风险,因为升级逻辑位于实现合约上,代理上除了
fallback()
之外没有逻辑,该逻辑委托调用到实现合约。 - 与 TPP 相比,运行时 gas 成本降低,因为代理不需要检查调用者是否为管理员。
- 部署新代理的成本降低,因为代理除了
fallback()
之外不包含任何逻辑。 - 降低了由于使用 EIP-1967 存储槽而导致的存储冲突风险。
- 区块浏览器兼容性。
缺点
- 由于升级逻辑存在于实现合约中,因此必须格外小心,以确保实现合约不能
selfdestruct
或因初始化不当而处于不良状态。如果实现合约出现问题,则代理无法恢复。 - 仍然会产生来自 Proxy 的
delegatecall
成本。
示例
- Superfluid
- Synthetix
- Github 上成百上千个项目
已知漏洞
- 未初始化的代理
- 函数冲突
- Selfdestruct
进一步阅读
-
EIP-1822
-
使用 UUPS 代理模式
-
用这个小技巧永久性地固定 UUPS 代理
-
- *
信标代理
你代理中的信标,是你的快乐让我感到惊讶吗?🤔
目前讨论的大多数代理将实现合约地址存储在代理合约存储中。信标模式由 Dharma 于 2019 年推广,它将实现合约的地址存储在一个单独的“信标”合约中。信标的地址使用 EIP-1967 存储模式存储在代理合约中。
使用其他类型的代理时,当实现合约升级时,所有代理都需要更新。但是,使用信标代理时,只需要更新信标合约本身。
代理中的信标地址以及信标上的实现合约地址都可以由管理员设置。这在处理需要以不同方式分组的大量代理合约时,提供了许多强大的组合。
实现地址 - 位于信标合约的唯一存储槽中。信标地址位于代理合约的唯一存储槽中。
升级逻辑 - 升级逻辑通常存在于信标合约中。
合约验证 - 是的,大多数 EVM 区块浏览器支持。
用例
- 如果你需要多个代理合约,可以通过升级信标一次性升级所有代理。
- 适用于涉及大量基于多个实现合约的代理合约的情况。信标代理模式允许同时更新各种代理组。
优点
- 更容易同时升级多个代理合约。
缺点
- 从存储中获取信标合约地址、调用信标合约,然后再从存储中获取实现合约地址的 gas 开销,还有使用代理所需的额外 gas。
- 增加了额外的复杂性。
示例
- USDC
- Dharma
已知漏洞
- 实现中不允许的 Delegatecall 和 selfdestruct
- 未初始化的代理
- 函数冲突
变体
- 不变的信标地址 - 为节省 gas,可以将信标地址在代理合约中设为不可变。实现合约仍可以通过更新信标进行设置。
- 无存储可升级信标代理 - 在这种模式中,信标合约并不在存储中存储实现合约地址,而是将其存储在代码中。代理合约通过
EXTCODECOPY
直接从信标加载它。
进一步阅读
-
如何创建信标代理
-
Dharma
-
- *
钻石代理
“钻石是代理最好的朋友吗?”
EIP-2535 “钻石”是能够在部署后进行升级/扩展的模块化智能合约系统,几乎没有大小限制。从 EIP:
钻石是一个具有外部函数的合约,这些函数由称为侧面的合约提供。侧面是可以共享内部函数、库和状态变量的独立合约。
钻石模式由一个中央的 Diamond.sol 代理合约组成。除了其他存储外,该合约包含可以在称为侧面的外部合约上调用的函数注册表。
钻石代理使用独特词汇的术语表:
钻石术语 | 定义 |
---|---|
钻石(Diamond) | 代理 |
侧面(Facet) | 实现 |
削减(Cut) | 升级 |
Loupe | 委托函数列表 |
完成的钻石 | 不可升级 |
单削减钻石 | 移除升级功能 |
合约验证 - 合约可以通过一个名为 Louper 的工具在 Etherscan 上进行验证( 示例 )。
用例
- 需要最高层级的可升级性和模块化互操作性的复杂系统。
优点
- 提供所需功能的稳定合约地址。通过单一地址发出事件可以简化事件处理。
- 可以用于分解超过 Spurious Dragon 限制的大型合约 > 24kb。
缺点
- 在路由函数时访问存储需要额外的 gas。
- 由于复杂性,存储冲突的机会增加。
- 当需要简单的可升级性时,复杂性可能过大。
示例
- Simple DeFi
- PartyFinance
- 示例完整列表。
已知漏洞
- 实现中不允许的 Delegatecall 和 selfdestruct
变体
- vtable
- 如何构建无大小限制的合约
进一步阅读
- 回答一些关于 Diamond 的问题
- 黑暗森林与 Diamond 标准
- 好主意,糟糕设计。Diamond 标准的不足之处
- 解决 Josselin Feist 对 EIP-2535 Diamonds 的担忧
变形合约
变形方法: “create2, 使用, selfdestruct, 再创建(新合约), 重复…”
变形合约与所有其他可升级模式的不同之处在于它不使用代理。没有对外部逻辑合约的 delegatecall
。
当需要升级时,变形合约使用 selfdestruct
,并使用 create2
将新合约部署到相同地址。
这可以通过让 initcode 从一个单独的外部合约的存储中检索创建代码来实现。通过这种方式,initcode 将始终相同,因此可以使用 create2
部署到相同地址。
不建议为新合约使用变形合约,因为 selfdestruct
操作码计划在不久的将来从以太坊中移除。有关详细信息,请参见 EIP-4758。
合约验证 - 是的,变形合约可以被验证。
用例
- 仅包含逻辑的合约(类似于 Solidity 外部库)。
- 状态较少且不频繁变化的合约,例如信标。
优点
- 不需要使用带有
delegatecall
的代理。 - 不需要使用
initialize()
而不是constructor()
。
缺点
- 升级时存储会被清除,因为使用了
selfdestruct
。 - 由于
selfdestruct
在交易结束时清除代码,因此升级需要两笔交易:一笔删除当前合约,另一笔创建新合约。在这两笔交易之间到达我们合约的任何交易都会失败。 selfdestruct
操作码可能在未来被移除。
示例
- 0age 的示例合约。
- 这更像是一种实验类型。主要用于 MEV 搜索者(etherscan 示例 在这里 和 在这里)。
已知漏洞
- 由于不使用代理或初始化器,因此不易受到典型的可升级代理漏洞的攻击。
- 在升级时可能会受到攻击。
进一步阅读
- 使用 CREATE2 的变形智能合约
- 变形合约的承诺与危险
- a16z 变形合约检测工具
我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~
版权声明
本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。