常见问题解答
当升级时,我可以更改 Solidity 编译器版本吗?
可以。Solidity 团队保证编译器会 跨版本保留存储布局。
为什么我会收到 "Cannot call fallback function from the proxy admin" 错误?
这是由于 透明代理模式 导致的。 当使用 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
的通用版本,允许手动禁用任何检查。
例如,为了升级到包含委托调用的实现,你可以调用:
await upgradeProxy(proxyAddress, implementationFactory, { unsafeAllow: ['delegatecall'] });
此外,可以直接从 Solidity 源代码中使用 NatSpec 注释精确地禁用检查。 这需要 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
关键字 来声明一个只能在构造期间分配一次并且只能在构造后读取的变量。 它通过在合约创建期间计算其值并将该值直接存储到字节码中来实现此目的。
请注意,此行为与可升级合约的工作方式不兼容,原因有两个:
-
可升级合约没有构造函数,但有初始化器,因此它们无法处理不可变变量。
-
由于不可变变量值存储在字节码中,因此其值将在指向给定合约的所有代理之间共享,而不是每个代理的存储。
在某些情况下,不可变变量是升级安全的。 插件目前无法自动检测到这些情况,因此无论如何都会将其指出为错误。 你可以使用选项 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
来禁用此检查。 一种更精细的方法是使用文档字符串注释 /// @custom:oz-renamed-from <previous name>
,直接位于被重命名的变量上方,例如:
contract V1 {
uint x;
}
contract V2 {
/// @custom:oz-renamed-from x
uint y;
}
也不允许更改变量的类型,即使在类型具有相同的大小和对齐方式的情况下,也是出于上述类似原因。 只要我们能保证布局的其余部分不受此类型更改的影响,也可以通过放置文档字符串注释 /// @custom:oz-retyped-from <previous type>
来覆盖此检查。
contract V1 {
bool x;
}
contract V2 {
/// @custom:oz-retyped-from bool
uint8 x;
}
由于当前的 Solidity 限制,文档字符串注释尚不适用于结构体成员。
如何在存储变量中使用内部函数?
存储变量中的内部函数是代码指针,升级后将不再有效,因为代码会移动并且指针会更改。 为了避免这个问题,你可以将这些函数声明为 external,或者完全避免存储中的代码指针,并定义一个 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;
}
...
}
然后在升级时,在升级过程中调用 reinitializer 函数,例如在 Hardhat 中:
await upgrades.upgradeProxy(PROXY_ADDRESS, ContractFactoryV2, {
call: 'initializeV2',
unsafeAllow: ['internal-function-storage']
});
或在 Foundry 中:
Upgrades.upgradeProxy(
PROXY_ADDRESS,
"V2.sol",
abi.encodeCall(V2.initializeV2, ())
);