本文是关于OpenZeppelin升级插件的常见问题解答,涵盖了Solidity编译器版本变更、常见错误、合约升级安全、禁用检查、使用delegatecall和selfdestruct、实现兼容性、代理管理员、实现合约、代理、immutable变量、外部库、升级函数、自定义类型以及在存储变量中使用内部函数等问题。
可以。Solidity 团队保证编译器将跨版本保留存储布局。
这是由于 Transparent Proxy Pattern 导致的。当使用 OpenZeppelin Upgrades Plugins 时,你不应该收到此错误,因为它使用 ProxyAdmin
合约来管理你的代理。
但是,如果你以编程方式使用 OpenZeppelin Contracts 代理,你可能会遇到此类错误。解决方案是从非代理管理员的帐户与你的代理进行交互,除非你特别要调用代理本身的功能。
当为合约部署代理时,合约代码有一些限制。特别是,合约不能有构造函数,并且出于安全原因,不应使用 selfdestruct
或 delegatecall
操作。
作为构造函数的替代方法,通常会设置一个 initialize
函数来处理合约的初始化。你可以使用 Initializable
基本合约来访问 initializer
修饰符,以确保该函数仅被调用一次。
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract MyContract is Initializable {
uint256 value;
function initialize(uint256 initialValue) public initializer {
value = initialValue;
}
}
这两个插件都将验证你尝试部署的合约是否符合这些规则。你可以在这里阅读更多关于如何编写升级安全合约的信息。
部署和升级相关的功能带有一个可选的 opts
对象,其中包括一个 unsafeAllow
选项。可以设置此选项以禁用插件执行的任何检查。可以单独禁用的检查列表是:
state-variable-assignment
state-variable-immutable
external-library-linking
struct-definition
enum-definition
constructor
delegatecall
selfdestruct
missing-public-upgradeto
internal-function-storage
此功能是原始 unsafeAllowCustomTypes
和 unsafeAllowLinkedLibraries
的通用版本,允许手动禁用任何检查。
例如,为了升级到包含 delegate call 的实现,你可以调用:
await upgradeProxy(proxyAddress, implementationFactory, { unsafeAllow: ['delegatecall'] });
此外,可以使用 NatSpec 注释直接从 Solidity 源代码中精确禁用检查。 这需要 Solidity >=0.8.2。
contract SomeContract {
function some_dangerous_function() public {
...
/// @custom:oz-upgrades-unsafe-allow delegatecall
(bool success, bytes memory returndata) = msg.sender.delegatecall("");
...
}
}
此语法可用于以下错误:
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
/// @custom:oz-upgrades-unsafe-allow state-variable-assignment
/// @custom:oz-upgrades-unsafe-allow external-library-linking
/// @custom:oz-upgrades-unsafe-allow constructor
/// @custom:oz-upgrades-unsafe-allow delegatecall
/// @custom:oz-upgrades-unsafe-allow selfdestruct
在某些情况下,你可能希望在单行中允许多个错误。
/// @custom:oz-upgrades-unsafe-allow constructor state-variable-immutable
contract SomeOtherContract {
uint256 immutable x;
constructor() {
x = block.number;
}
}
你还可以使用以下方法来允许在可访问代码中的特定错误,其中包括任何被引用的合约、函数和库:
/// @custom:oz-upgrades-unsafe-allow-reachable delegatecall
/// @custom:oz-upgrades-unsafe-allow-reachable selfdestruct
delegatecall
和 selfdestruct
吗?这是一种高级技术,可能会使资金面临永久损失的风险。 |
如果对 delegatecall
和 selfdestruct
进行保护,以便只能通过代理触发它们,而不能在实现合约本身上触发它们,则可以安全地使用它们。 在 Solidity 中实现此目的的一种方法如下。
abstract contract OnlyDelegateCall {
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address private immutable self = address(this);
function checkDelegateCall() private view {
require(address(this) != self);
}
modifier onlyDelegateCall() {
checkDelegateCall();
_;
}
}
contract UsesUnsafeOperations is OnlyDelegateCall {
/// @custom:oz-upgrades-unsafe-allow selfdestruct
function destroyProxy() onlyDelegateCall {
selfdestruct(msg.sender);
}
}
当将代理从一个实现升级到另一个实现时,两个实现的 存储布局 必须兼容。 这意味着,即使你可以完全更改实现的代码,也不能修改现有的合约状态变量。 唯一允许的操作是在已声明的状态变量之后附加新的状态变量。
这两个插件都将验证新的实现合约是否与之前的合约兼容。
你可以在这里阅读更多关于如何对实现合约进行存储兼容更改的信息。
ProxyAdmin
是一个中间合约,充当透明代理的升级者。 每个 ProxyAdmin
都由部署者地址或从 OpenZeppelin Contracts 5.0 或更高版本部署透明代理时由 initialOwner
地址拥有。 你可以通过调用 transferOwnership
来转移代理管理员的所有权。
可升级部署至少需要两个合约:代理和实现。 代理合约是你和你的用户将与之交互的实例,而实现是保存代码的合约。 如果你为同一个实现合约多次调用 deployProxy
,则会部署多个代理,但只会使用一个实现合约。
当你将代理升级到新版本时,如果需要,会部署一个新的实现合约,并将代理设置为使用新的实现合约。 你可以在这里阅读更多关于代理升级模式的信息。
代理是一个将其所有调用委托给第二个合约(称为实现合约)的合约。 所有状态和资金都保存在代理中,但实际执行的代码是实现的的代码。 代理可以由其管理员 升级 为使用不同的实现合约。
你可以在这里阅读更多关于代理升级模式的信息。
immutable
变量?Solidity 0.6.5 引入了 immutable
关键字来声明一个只能在构造期间赋值一次的变量,并且只能在构造后读取。 它通过在合约创建期间计算其值并将该值直接存储到字节码中来实现。
请注意,此行为与可升级合约的工作方式不兼容,原因有两个:
可升级合约没有构造函数,但有初始化器,因此它们无法处理 immutable 变量。
由于 immutable 变量值存储在字节码中,因此其值将在指向给定合约的所有代理之间共享,而不是每个代理的存储。
在某些情况下,immutable 变量是升级安全的。 目前,插件无法自动检测这些情况,因此它们仍然会将其标记为错误。 你可以使用选项 unsafeAllow: ['state-variable-immutable'] 手动禁用此检查,或在 Solidity >=0.8.2 中,在变量声明前放置注释 /// @custom:oz-upgrades-unsafe-allow state-variable-immutable 。 |
目前,这些插件仅部分支持链接到外部库的可升级合约。 这是因为在编译时不知道将要链接的实现,因此很难保证升级操作的安全性。
计划在不久的将来添加此功能,但需要一些约束条件才能使问题更容易解决,例如假设外部库的源代码存在于代码库中,或者已部署并挖掘该源代码,因此可以从区块链中获取以进行分析。
与此同时,你可以通过在 deployProxy
或 upgradeProxy
调用中将 unsafeAllowLinkedLibraries
标志设置为 true,或者在 unsafeAllow
数组中包含 'external-library-linking'
来部署链接到外部库的可升级合约。 请记住,插件不会验证链接的库是否升级安全。 在完全支持外部库之前,这必须手动完成。
你可以关注或为此问题做出贡献在 GitHub 上。
upgradeTo
或 upgradeToAndCall
函数?当使用 UUPS 代理(通过 kind: 'uups'
选项)时,实现合约必须包含公共函数 upgradeTo(address newImplementation)
或 upgradeToAndCall(address newImplementation, bytes memory data)
中的一个或两个。 这是因为在 UUPS 模式中,代理本身不包含升级功能,并且整个可升级性机制都存在于实现端。 因此,在每次部署和升级时,我们都必须确保包含它,否则我们可能会永久禁用合约的可升级性。
包含这些函数中的一个或两个的推荐方法是继承 OpenZeppelin Contracts 中提供的 UUPSUpgradeable
合约,如下所示。 该合约添加了所需的功能,但还包含一个内置机制,该机制将在升级时在链上检查所提出的新实现也继承了 UUPSUpgradeable
或实现了相同的接口。 这样,当使用 Upgrades Plugins 时,就有两层缓解措施来防止意外禁用可升级性:插件的链下检查和合约本身的链上回退。
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract MyContract is Initializable, ..., UUPSUpgradeable {
...
}
在 Transparent vs UUPS 中阅读更多关于与透明代理模式的区别。
插件的过去版本不支持在其代码或链接库中使用结构体或枚举等自定义类型的可升级合约。 对于当前版本的插件来说,情况不再如此,并且在升级合约时将自动检查结构体和枚举的兼容性。
一些已经部署了带有结构体和/或枚举的代理并且需要升级这些代理的用户可能需要在下次升级时使用覆盖标志 unsafeAllowCustomTypes
,之后就不再需要了。 如果项目包含代理当前使用的实现的源代码,则插件将尝试在升级之前恢复其需要的元数据,如果不可能,则回退到覆盖标志。
默认情况下不允许重命名变量,因为重命名实际上可能是意外的重新排序。 例如,如果变量 uint a; uint b;
被升级到 uint b; uint a;
,如果只是简单地允许重命名,这不会被视为错误,但它可能是一个意外,尤其是在涉及多重继承时。
可以通过传递选项 unsafeAllowRenames: true
来禁用此检查。 一种更细粒度的方法是在正在重命名的变量的正上方使用 docstring 注释 /// @custom:oz-renamed-from <previous name>
,例如:
contract V1 {
uint x;
}
contract V2 {
/// @custom:oz-renamed-from x
uint y;
}
也不允许更改变量的类型,即使在类型具有相同大小和对齐方式的情况下也是如此,原因与上面解释的类似。 只要我们可以保证布局的其余部分不受此类型更改的影响,也可以通过放置 docstring 注释 /// @custom:oz-retyped-from <previous type>
来覆盖此检查。
contract V1 {
bool x;
}
contract V2 {
/// @custom:oz-retyped-from bool
uint8 x;
}
由于当前 Solidity 的限制,Docstring 注释尚不适用于结构体成员。
存储变量中的内部函数是代码指针,在升级后将不再有效,因为代码会移动并且指针会更改。 为避免此问题,你可以将这些函数声明为外部函数,或者完全避免存储中的代码指针,并定义一个 enum
,你将使用它和一个分发器函数来从可用函数列表中选择。 如果你必须使用内部函数,则需要在每次升级期间重新分配这些内部函数。
例如,以下合约中的 messageFunction
变量不是升级安全的。 在升级后尝试调用 showMessage()
很可能会导致 revert。
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract V1 is Initializable {
function() internal pure returns (string memory) messageFunction;
function initialize() initializer public {
messageFunction = hello;
}
function hello() internal pure returns (string memory) {
return "Hello, World!";
}
function showMessage() public view returns (string memory) {
return messageFunction();
}
...
}
为了允许 Upgrades Plugins 部署上述合约,你可以根据如何禁用某些检查?禁用 internal-function-storage
检查,但请确保按照以下步骤在升级期间重新分配内部函数。
在此合约的新版本中,再次在存储变量中分配内部函数,例如通过使用 reinitializer:
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "./V1.sol";
contract V2 is V1 {
function initializeV2() reinitializer(2) public {
messageFunction = hello;
}
...
}
然后在升级时,将重新初始化器函数作为升级过程的一部分调用,例如在 Hardhat 中:
await upgrades.upgradeProxy(PROXY_ADDRESS, ContractFactoryV2, {
call: 'initializeV2',
unsafeAllow: ['internal-function-storage']
});
或在 Foundry 中:
Upgrades.upgradeProxy(
PROXY_ADDRESS,
"V2.sol",
abi.encodeCall(V2.initializeV2, ())
);
- 原文链接: docs.openzeppelin.com/up...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!