本文档介绍了OpenZeppelin Contracts库中关于代理(Proxy)模式的各种实现,包括基本代理、ERC1967代理、透明代理(Transparent Proxy)和 UUPS 代理,以及 Beacon 代理和最小克隆代理。
你没有在阅读此文档的当前版本。5.x 是当前版本。
建议在 https://docs.openzeppelin.com/contracts/api/proxy 查看此文档 |
这是一组底层合约,实现了不同带或不带升级能力的代理模式。有关此模式的深入概述,请查看 代理升级模式 页面。
以下大多数代理都建立在抽象基础合约之上。
Proxy
:实现核心委托功能的抽象合约。为了避免与代理背后实现合约的存储变量冲突,我们使用 EIP1967 存储槽。
ERC1967Upgrade
:用于获取和设置 EIP1967 中定义的存储槽的内部函数。
ERC1967Proxy
:使用 EIP1967 存储槽的代理。默认情况下不可升级。
有两种替代方法可以向 ERC1967 代理添加升级能力。它们的区别在下面的 透明代理与 UUPS 代理 中解释。
TransparentUpgradeableProxy
:具有内置管理员和升级接口的代理。
UUPSUpgradeable
:要包含在实现合约中的升级机制。
正确且安全地使用可升级代理是一项困难的任务,需要深入了解代理模式、Solidity 和 EVM。除非你想要大量的底层控制,否则我们建议使用 OpenZeppelin Upgrades Plugins for Truffle 和 Hardhat。 |
另一种不同的代理系列是信标代理(beacon proxies)。这种由 Dharma 推广的模式允许在单个事务中将多个代理升级到不同的实现。
BeaconProxy
:一种从信标合约检索其实现的代理。
UpgradeableBeacon
:具有内置管理员的信标合约,可以升级指向它的 BeaconProxy
。
在这种模式中,代理合约不像 ERC1967 代理那样在存储中保存实现地址。相反,该地址存储在一个单独的信标合约中。 upgrade
操作被发送到信标而不是代理合约,并且所有遵循该信标的代理都会自动升级。
在升级能力之外,代理还可以用于制作廉价的合约克隆,例如由链上工厂合约创建的克隆,该工厂合约创建同一合约的多个实例。这些实例旨在部署成本低廉,调用成本也低廉。
Clones
:一个可以部署廉价的最小的不可升级代理的库。OpenZeppelin 中最初包含的代理遵循 透明代理模式。虽然仍然提供此模式,但我们现在的建议正在转向 UUPS 代理,它既轻量级又通用。UUPS 这个名称来自 EIP1822,它首次记录了该模式。
虽然这两种代理共享相同的升级接口,但在 UUPS 代理中,升级由实现处理,并最终可以删除。另一方面,透明代理在代理本身中包含升级和管理逻辑。这意味着部署 TransparentUpgradeableProxy
的成本高于 UUPS 代理。
UUPS 代理是使用 ERC1967Proxy
实现的。请注意,此代理本身不可升级。实现的作用是包括合约的逻辑以及更新存储在代理存储空间中特定槽中的实现地址所需的所有代码。这就是 UUPSUpgradeable
合约的用武之地。继承它(并使用相关的访问控制机制覆盖 _authorizeUpgrade
函数)会将你的合约变成符合 UUPS 的实现。
请注意,由于这两个代理都使用相同的存储槽来存储实现地址,因此将符合 UUPS 的实现与 TransparentUpgradeableProxy
一起使用可能会允许非管理员执行升级操作。
默认情况下,UUPSUpgradeable
中包含的升级功能包含一种安全机制,可以防止升级到不符合 UUPS 的实现。这可以防止升级到不包含必要升级机制的实现合约,因为它会永远锁定代理的升级能力。可以通过以下两种方式绕过此安全机制:
在实现中添加一个标志机制,当触发时将禁用升级功能。
升级到具有不包含额外安全检查的升级机制的实现,然后再次升级到没有升级机制的另一个实现。
此安全机制的当前实现使用 EIP1822 来检测实现使用的存储槽。先前的实现(现已弃用)依赖于回滚检查。可以从使用旧机制的合约升级到新机制。然而,反过来是不可能的,因为旧的实现(4.5 版本之前)不包含 ERC1822
接口。
Proxy
import "@openzeppelin/contracts/proxy/Proxy.sol";
这个抽象合约提供了一个 fallback 函数,它使用 EVM 指令 delegatecall
将所有调用委托给另一个合约。我们将第二个合约称为代理背后的 实现,并且必须通过覆盖虚拟的 _implementation
函数来指定它。
此外,可以通过 _fallback
函数手动触发对实现的委托,或者通过 _delegate
函数触发对不同合约的委托。
委托调用的成功和返回数据将返回给代理的调用者。
函数
_delegate(address implementation)
internal将当前调用委托给 implementation
。
此函数不会返回到其内部调用站点,它将直接返回到外部调用者。
_implementation() → address
internal这是一个虚拟函数,应该被覆盖,以便它返回 fallback 函数和 _fallback
应该委托到的地址。
_fallback()
internal将当前调用委托给 _implementation()
返回的地址。
此函数不会返回到其内部调用站点,它将直接返回到外部调用者。
fallback()
externalFallback 函数,将调用委托给 _implementation()
返回的地址。如果合约中没有其他函数与调用数据匹配,则将运行此函数。
receive()
externalFallback 函数,将调用委托给 _implementation()
返回的地址。如果调用数据为空,则将运行此函数。
_beforeFallback()
internal在 fallback 到实现之前调用的Hook。可以作为手动 _fallback
调用的一部分发生,也可以作为 Solidity fallback
或 receive
函数的一部分发生。
如果被覆盖,应该调用 super._beforeFallback()
。
ERC1967Proxy
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
此合约实现了一个可升级的代理。它是可升级的,因为调用被委托给一个可以更改的实现地址。此地址存储在 EIP1967 指定的位置的存储中,因此它不会与代理背后的实现的存储布局冲突。
函数
ERC1967Upgrade
Proxy
事件
IERC1967
constructor(address _logic, bytes _data)
public使用 _logic
指定的初始实现来初始化可升级代理。
如果 _data
非空,则将其用作委托调用 _logic
中的数据。这通常是一个编码的函数调用,并允许像 Solidity 构造函数一样初始化代理的存储。
_implementation() → address impl
internal返回当前的实现地址。
ERC1967Upgrade
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol";
这个抽象合约为 EIP1967 槽提供了 getter 和事件发送更新函数。
自 v4.1 起可用。
函数
事件
IERC1967
_getImplementation() → address
internal返回当前的实现地址。
_upgradeTo(address newImplementation)
internal执行实现升级
发出一个 {Upgraded} 事件。
_upgradeToAndCall(address newImplementation, bytes data, bool forceCall)
internal执行实现升级,并进行额外的设置调用。
发出一个 {Upgraded} 事件。
_upgradeToAndCallUUPS(address newImplementation, bytes data, bool forceCall)
internal执行实现升级,并进行 UUPS 代理的安全检查,以及额外的设置调用。
发出一个 {Upgraded} 事件。
_getAdmin() → address
internal返回当前的管理地址。
_changeAdmin(address newAdmin)
internal更改代理的管理地址。
发出一个 {AdminChanged} 事件。
_getBeacon() → address
internal返回当前的信标地址。
_upgradeBeaconToAndCall(address newBeacon, bytes data, bool forceCall)
internal执行信标升级,并进行额外的设置调用。注意:这会升级信标的地址,它不会升级信标中包含的实现(请参阅 UpgradeableBeacon._setImplementation
)。
发出一个 {BeaconUpgraded} 事件。
TransparentUpgradeableProxy
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
此合约实现了一个可由管理员升级的代理。
为了避免 代理选择器冲突,该冲突可能被用于攻击,此合约使用了 透明代理模式。此模式意味着两个密切相关的事情:
如果除管理员以外的任何帐户调用代理,则该调用将转发到实现,即使该调用与代理本身公开的管理员函数之一匹配。
如果管理员调用代理,则它可以访问管理员函数,但其调用永远不会转发到实现。如果管理员尝试调用实现中的函数,则将失败,并显示一条错误消息,指出“管理员无法 fallback 到代理目标”。
这些属性意味着管理帐户只能用于管理操作,例如升级代理或更改管理员,因此最好是专用的帐户,不用于其他任何操作。这将避免在尝试从代理实现调用函数时由于突然出现的错误而带来的麻烦。
我们的建议是将专用帐户作为 ProxyAdmin
合约的实例。如果以这种方式设置,则应将 ProxyAdmin
实例视为代理的真实管理界面。
此代理的真实接口是在 ITransparentUpgradeableProxy 中定义的。此合约不继承该接口,而是使用 _fallback 中的自定义调度机制隐式地实现管理函数。因此,编译器不会为此合约生成 ABI。这对于完全实现透明性而不会解码由代理和实现之间的选择器冲突引起的回滚是必要的。 |
不建议扩展此合约以添加其他外部函数。如果这样做,编译器将不会检查是否存在选择器冲突,因为上述说明。任何新函数与 ITransparentUpgradeableProxy 中声明的函数之间的选择器冲突将有利于新函数。这可能会使管理操作无法访问,从而阻止升级能力。透明性也可能受到损害。 |
修饰符
函数
ERC1967Proxy
ERC1967Upgrade
Proxy
事件
IERC1967
ifAdmin()
modifier内部使用的修饰符,除非发送者是管理员,否则会将调用委托给实现。
此修饰符已弃用,因为它可能会导致问题,如果修改后的函数具有参数,并且实现提供了具有相同选择器的函数。 |
constructor(address _logic, address admin_, bytes _data)
public初始化一个由 _admin
管理的可升级代理,该代理由 _logic
上的实现提供支持,并且可以选择使用 _data
进行初始化,如 ERC1967Proxy.constructor
中所述。
_fallback()
internal如果调用者是管理员,则在内部处理调用,否则透明地 fallback 到代理行为
_admin() → address
internal返回当前的管理员。
此函数已弃用。请改用 ERC1967Upgrade._getAdmin 。 |
ProxyAdmin
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
这是一个辅助合约,旨在分配为 TransparentUpgradeableProxy
的管理员。有关为什么要使用它的解释,请参阅 TransparentUpgradeableProxy
的文档。
函数
Ownable
事件
Ownable
getProxyImplementation(contract ITransparentUpgradeableProxy proxy) → address
public返回 proxy
的当前实现。
要求:
proxy
的管理员。getProxyAdmin(contract ITransparentUpgradeableProxy proxy) → address
public返回 proxy
的当前管理员。
要求:
proxy
的管理员。changeProxyAdmin(contract ITransparentUpgradeableProxy proxy, address newAdmin)
public将 proxy
的管理员更改为 newAdmin
。
要求:
proxy
的当前管理员。upgrade(contract ITransparentUpgradeableProxy proxy, address implementation)
public将 proxy
升级到 implementation
。请参阅 {TransparentUpgradeableProxy-upgradeTo}。
要求:
proxy
的管理员。upgradeAndCall(contract ITransparentUpgradeableProxy proxy, address implementation, bytes data)
public将 proxy
升级到 implementation
并调用新实现上的函数。请参阅 {TransparentUpgradeableProxy-upgradeToAndCall}。
要求:
proxy
的管理员。BeaconProxy
import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
此合约实现了一个代理,该代理从 UpgradeableBeacon
获取每次调用的实现地址。
信标地址存储在存储槽 uint256(keccak256('eip1967.proxy.beacon')) - 1
中,因此它不会与代理背后的实现的存储布局冲突。
自 v3.4 起可用。
函数
ERC1967Upgrade
Proxy
事件
IERC1967
constructor(address beacon, bytes data)
public使用 beacon
初始化代理。
如果 data
非空,则将其用作委托调用信标返回的实现中的数据。这通常是一个编码的函数调用,并允许像 Solidity 构造函数一样初始化代理的存储。
要求:
beacon
必须是具有 IBeacon
接口的合约。_beacon() → address
internal返回当前的信标地址。
_implementation() → address
internal返回关联信标的当前实现地址。
_setBeacon(address beacon, bytes data)
internal更改代理以使用新的信标。已弃用:请参阅 _upgradeBeaconToAndCall
。
如果 data
非空,则将其用作委托调用信标返回的实现中的数据。
要求:
beacon
必须是一个合约。
beacon
返回的实现必须是一个合约。
IBeacon
import "@openzeppelin/contracts/proxy/beacon/IBeacon.sol";
这是 BeaconProxy
对其信标的期望的接口。
函数
implementation() → address
external必须返回一个可用作委托调用目标的地址。
BeaconProxy
将检查此地址是否为合约。
UpgradeableBeacon
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
此合约与一个或多个 BeaconProxy
实例结合使用,以确定它们的实现合约,它们将在其中委托所有函数调用。
所有者能够更改信标指向的实现,从而升级使用此信标的代理。
函数
Ownable
事件
Ownable
constructor(address implementation_)
public设置初始实现的地址,并将部署者帐户设置为可以升级信标的所有者计算使用 Clones.cloneDeterministic
部署的克隆的地址。
predictDeterministicAddress(address implementation, bytes32 salt) → address predicted
internal计算使用 Clones.cloneDeterministic
部署的克隆的地址。
Initializable
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
这是一个基础合约,用于辅助编写可升级合约,或任何将部署在代理后面的合约。由于代理合约不使用构造函数,因此通常将构造函数逻辑移动到外部初始化函数,通常称为 initialize
。然后,有必要保护此初始化函数,使其只能被调用一次。此合约提供的 initializer
修饰符将具有此效果。
初始化函数使用版本号。一旦使用了一个版本号,它就会被消耗掉,不能重复使用。这种机制可以防止重复执行每个 “步骤”,但允许在升级添加需要初始化的模块时创建新的初始化步骤。
例如:
contract MyToken is ERC20Upgradeable {
function initialize() initializer public {
__ERC20_init("MyToken", "MTK");
}
}
contract MyTokenV2 is MyToken, ERC20PermitUpgradeable {
function initializeV2() reinitializer(2) public {
__ERC20Permit_init("MyToken");
}
}
为了避免将代理保留在未初始化的状态,应该通过将编码的函数调用作为 _data 参数提供给 ERC1967Proxy.constructor ,尽早调用初始化函数。 |
当与继承一起使用时,必须手动注意不要两次调用父初始化程序,或者确保所有初始化程序都是幂等的。这不会像 Solidity 那样自动验证构造函数。 |
避免将合约保持未初始化状态。<br>未初始化的合约可能被攻击者接管。这既适用于代理及其实现<br>合约,也可能影响代理。为了防止使用实现合约,你应该调用<br>_disableInitializers 函数在构造函数中,以便在部署时自动锁定它:<br>none hljs<br>/// @custom:oz-upgrades-unsafe-allow constructor<br>constructor() {<br> _disableInitializers();<br>}<br> |
修饰符
函数
事件
initializer()
修饰符一个修饰符,用于定义一个受保护的初始化函数,该函数最多可以被调用一次。在其作用域内,onlyInitializing
函数可用于初始化父合约。
与 reinitializer(1)
类似,只是用 initializer
标记的函数可以嵌套在构造函数的上下文中。
发出 Initialized
事件。
reinitializer(uint8 version)
修饰符一个修饰符,用于定义一个受保护的重新初始化函数,该函数最多可以被调用一次,并且只有在该合约之前没有被初始化为更大的版本时才能被调用。在其作用域内,onlyInitializing
函数可用于初始化父合约。
重新初始化程序可以在原始初始化步骤之后使用。这对于配置通过升级添加的并且需要初始化的模块至关重要。
当 version
为 1 时,此修饰符类似于 initializer
,只是用 reinitializer
标记的函数不能嵌套。如果在另一个函数的上下文中调用一个函数,则执行将恢复。
请注意,版本可以跳跃大于 1 的增量;这意味着如果多个重新初始化程序共存于一个合约中,则以正确的顺序执行它们取决于开发人员或运营商。
将版本设置为 255 将阻止任何未来的重新初始化。 |
发出 Initialized
事件。
onlyInitializing()
修饰符修饰符用于保护初始化函数,使其只能由具有 initializer
和 reinitializer
修饰符的函数直接或间接调用。
_disableInitializers()
internal锁定合约,防止任何未来的重新初始化。这不能是初始化调用的一部分。 在合约的构造函数中调用此函数将阻止该合约被初始化或重新初始化 到任何版本。建议使用此方法来锁定设计为通过代理调用的实现合约。
第一次成功执行时,发出 Initialized
事件。
_getInitializedVersion() → uint8
internal返回已初始化的最高版本。请参阅 reinitializer
。
_isInitializing() → bool
internal如果合约当前正在初始化,则返回 true
。请参阅 onlyInitializing
。
Initialized(uint8 version)
event当合约已初始化或重新初始化时触发。
UUPSUpgradeable
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
一种为 UUPS 代理设计的可升级性机制。此处包含的函数可以执行 ERC1967Proxy
的升级,当此合约设置为此类代理后面的实现时。
一种安全机制确保升级不会意外关闭可升级性,尽管如果升级保留了可升级性但删除了安全机制,则此风险会恢复,例如,通过将 UUPSUpgradeable
替换为升级的自定义实现。
必须覆盖 _authorizeUpgrade
函数,以包括对升级机制的访问限制。
自 v4.1 版本起可用。
修饰符
函数
ERC1967Upgrade
事件
IERC1967
onlyProxy()
修饰符检查执行是否通过委托调用执行,并且执行上下文是 一个代理合约,其实现(如 ERC1967 中定义)指向自身。这应该只发生在 对于使用当前合约作为其实现的 UUPS 和透明代理。通过 ERC1167 最小代理(克隆)执行函数通常不会通过此测试,但不保证 失败。
notDelegated()
修饰符检查执行是否未通过委托调用执行。 这允许在实现合约上调用函数,但不能通过代理调用。
proxiableUUID() → bytes32
externalERC1822 的 proxiableUUID
函数的实现。这将返回实现使用的存储槽。它用于在执行升级时验证实现的兼容性。
指向可代理合约的代理本身不应被视为可代理,因为这可能导致<br>通过委托给自己直到耗尽 gas 来破坏升级到它的代理。因此,至关重要的是,如果通过代理调用此<br>函数,则此函数会恢复。 notDelegated 修饰符保证了这一点。 |
upgradeTo(address newImplementation)
public将代理的实现升级到 newImplementation
。
发出 Upgraded
事件。
upgradeToAndCall(address newImplementation, bytes data)
public将代理的实现升级到 newImplementation
,然后执行编码在 data
中的函数调用。
发出 Upgraded
事件。
_authorizeUpgrade(address newImplementation)
internal当 msg.sender
未被授权升级合约时应还原的函数。被
upgradeTo
和 upgradeToAndCall
调用。
通常,此函数将使用 访问控制 修饰符,例如 Ownable.onlyOwner
。
function _authorizeUpgrade(address) internal override onlyOwner {}
- 原文链接: docs.openzeppelin.com/co...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!