Simplify UUPSUpgradeable along the lines of ERC1822 (#3021)
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
This commit is contained in:
20
contracts/interfaces/draft-IERC1822.sol
Normal file
20
contracts/interfaces/draft-IERC1822.sol
Normal file
@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts v4.x.0 (proxy/ERC1822/IProxiable.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
/**
|
||||
* @dev ERC1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified
|
||||
* proxy whose upgrades are fully controlled by the current implementation.
|
||||
*/
|
||||
interface IERC1822Proxiable {
|
||||
/**
|
||||
* @dev Returns the storage slot that the proxiable contract assumes is being used to store the implementation
|
||||
* address.
|
||||
*
|
||||
* IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks
|
||||
* bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this
|
||||
* function revert if invoked through a proxy.
|
||||
*/
|
||||
function proxiableUUID() external view returns (bytes32);
|
||||
}
|
||||
58
contracts/mocks/UUPS/UUPSLegacy.sol
Normal file
58
contracts/mocks/UUPS/UUPSLegacy.sol
Normal file
@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "./UUPSUpgradeableMock.sol";
|
||||
|
||||
// This contract implements the pre-4.5 UUPS upgrade function with a rollback test.
|
||||
// It's used to test that newer UUPS contracts are considered valid upgrades by older UUPS contracts.
|
||||
contract UUPSUpgradeableLegacyMock is UUPSUpgradeableMock {
|
||||
// Inlined from ERC1967Upgrade
|
||||
bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143;
|
||||
|
||||
// ERC1967Upgrade._setImplementation is private so we reproduce it here.
|
||||
// An extra underscore prevents a name clash error.
|
||||
function __setImplementation(address newImplementation) private {
|
||||
require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
|
||||
StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
|
||||
}
|
||||
|
||||
function _upgradeToAndCallSecureLegacyV1(
|
||||
address newImplementation,
|
||||
bytes memory data,
|
||||
bool forceCall
|
||||
) internal {
|
||||
address oldImplementation = _getImplementation();
|
||||
|
||||
// Initial upgrade and setup call
|
||||
__setImplementation(newImplementation);
|
||||
if (data.length > 0 || forceCall) {
|
||||
Address.functionDelegateCall(newImplementation, data);
|
||||
}
|
||||
|
||||
// Perform rollback test if not already in progress
|
||||
StorageSlot.BooleanSlot storage rollbackTesting = StorageSlot.getBooleanSlot(_ROLLBACK_SLOT);
|
||||
if (!rollbackTesting.value) {
|
||||
// Trigger rollback using upgradeTo from the new implementation
|
||||
rollbackTesting.value = true;
|
||||
Address.functionDelegateCall(
|
||||
newImplementation,
|
||||
abi.encodeWithSignature("upgradeTo(address)", oldImplementation)
|
||||
);
|
||||
rollbackTesting.value = false;
|
||||
// Check rollback was effective
|
||||
require(oldImplementation == _getImplementation(), "ERC1967Upgrade: upgrade breaks further upgrades");
|
||||
// Finally reset to the new implementation and log the upgrade
|
||||
_upgradeTo(newImplementation);
|
||||
}
|
||||
}
|
||||
|
||||
// hooking into the old mechanism
|
||||
function upgradeTo(address newImplementation) external virtual override {
|
||||
_upgradeToAndCallSecureLegacyV1(newImplementation, bytes(""), false);
|
||||
}
|
||||
|
||||
function upgradeToAndCall(address newImplementation, bytes memory data) external payable virtual override {
|
||||
_upgradeToAndCallSecureLegacyV1(newImplementation, data, false);
|
||||
}
|
||||
}
|
||||
@ -19,13 +19,3 @@ contract UUPSUpgradeableUnsafeMock is UUPSUpgradeableMock {
|
||||
ERC1967Upgrade._upgradeToAndCall(newImplementation, data, false);
|
||||
}
|
||||
}
|
||||
|
||||
contract UUPSUpgradeableBrokenMock is UUPSUpgradeableMock {
|
||||
function upgradeTo(address) external virtual override {
|
||||
// pass
|
||||
}
|
||||
|
||||
function upgradeToAndCall(address, bytes memory) external payable virtual override {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
pragma solidity ^0.8.2;
|
||||
|
||||
import "../beacon/IBeacon.sol";
|
||||
import "../../interfaces/draft-IERC1822.sol";
|
||||
import "../../utils/Address.sol";
|
||||
import "../../utils/StorageSlot.sol";
|
||||
|
||||
@ -77,33 +78,23 @@ abstract contract ERC1967Upgrade {
|
||||
*
|
||||
* Emits an {Upgraded} event.
|
||||
*/
|
||||
function _upgradeToAndCallSecure(
|
||||
function _upgradeToAndCallUUPS(
|
||||
address newImplementation,
|
||||
bytes memory data,
|
||||
bool forceCall
|
||||
) internal {
|
||||
address oldImplementation = _getImplementation();
|
||||
|
||||
// Initial upgrade and setup call
|
||||
_setImplementation(newImplementation);
|
||||
if (data.length > 0 || forceCall) {
|
||||
Address.functionDelegateCall(newImplementation, data);
|
||||
}
|
||||
|
||||
// Perform rollback test if not already in progress
|
||||
StorageSlot.BooleanSlot storage rollbackTesting = StorageSlot.getBooleanSlot(_ROLLBACK_SLOT);
|
||||
if (!rollbackTesting.value) {
|
||||
// Trigger rollback using upgradeTo from the new implementation
|
||||
rollbackTesting.value = true;
|
||||
Address.functionDelegateCall(
|
||||
newImplementation,
|
||||
abi.encodeWithSignature("upgradeTo(address)", oldImplementation)
|
||||
);
|
||||
rollbackTesting.value = false;
|
||||
// Check rollback was effective
|
||||
require(oldImplementation == _getImplementation(), "ERC1967Upgrade: upgrade breaks further upgrades");
|
||||
// Finally reset to the new implementation and log the upgrade
|
||||
_upgradeTo(newImplementation);
|
||||
// Upgrades from old implementations will perform a rollback test. This test requires the new
|
||||
// implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing
|
||||
// this special case will break upgrade paths from old UUPS implementation to new ones.
|
||||
if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) {
|
||||
_setImplementation(newImplementation);
|
||||
} else {
|
||||
try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) {
|
||||
require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID");
|
||||
} catch {
|
||||
revert("ERC1967Upgrade: new implementation is not UUPS");
|
||||
}
|
||||
_upgradeToAndCall(newImplementation, data, forceCall);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,14 +17,14 @@ In order to avoid clashes with the storage variables of the implementation contr
|
||||
There are two alternative ways to add upgradeability to an ERC1967 proxy. Their differences are explained below in <<transparent-vs-uups>>.
|
||||
|
||||
- {TransparentUpgradeableProxy}: A proxy with a built in admin and upgrade interface.
|
||||
- {UUPSUpgradeable}: An upgradeability mechanism to be included in the implementation for an ERC1967 proxy.
|
||||
- {UUPSUpgradeable}: An upgradeability mechanism to be included in the implementation contract.
|
||||
|
||||
CAUTION: Using upgradeable proxies correctly and securely is a difficult task that requires deep knowledge of the proxy pattern, Solidity, and the EVM. Unless you want a lot of low level control, we recommend using the xref:upgrades-plugins::index.adoc[OpenZeppelin Upgrades Plugins] for Truffle and Hardhat.
|
||||
|
||||
A different family of proxies are beacon proxies. This pattern, popularized by Dharma, allows multiple proxies to be upgraded to a different implementation in a single transaction.
|
||||
|
||||
- {BeaconProxy}: A proxy that retreives its implementation from a beacon contract.
|
||||
- {UpgradeableBeacon}: A beacon contract that can be upgraded.
|
||||
- {UpgradeableBeacon}: A beacon contract with a built in admin that can upgrade the {BeaconProxy} pointing to it.
|
||||
|
||||
In this pattern, the proxy contract doesn't hold the implementation address in storage like an ERC1967 proxy, instead the address is stored in a separate beacon contract. The `upgrade` operations that are sent to the beacon instead of to the proxy contract, and all proxies that follow that beacon are automatically upgraded.
|
||||
|
||||
@ -48,6 +48,8 @@ By default, the upgrade functionality included in {UUPSUpgradeable} contains a s
|
||||
- Adding a flag mechanism in the implementation that will disable the upgrade function when triggered.
|
||||
- Upgrading to an implementation that features an upgrade mechanism without the additional security check, and then upgrading again to another implementation without the upgrade mechanism.
|
||||
|
||||
The current implementation of this security mechanism uses https://eips.ethereum.org/EIPS/eip-1822[EIP1822] to detect the storage slot used by the implementation. A previous implementation, now deprecated, relied on a rollback check. It is possible to upgrade from a contract using the old mechanism to a new one. The inverse is however not possible, as old implementations (before version 4.5) did not include the `ERC1822` interface.
|
||||
|
||||
== Core
|
||||
|
||||
{{Proxy}}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "../../interfaces/draft-IERC1822.sol";
|
||||
import "../ERC1967/ERC1967Upgrade.sol";
|
||||
|
||||
/**
|
||||
@ -17,7 +18,7 @@ import "../ERC1967/ERC1967Upgrade.sol";
|
||||
*
|
||||
* _Available since v4.1._
|
||||
*/
|
||||
abstract contract UUPSUpgradeable is ERC1967Upgrade {
|
||||
abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade {
|
||||
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment
|
||||
address private immutable __self = address(this);
|
||||
|
||||
@ -34,6 +35,27 @@ abstract contract UUPSUpgradeable is ERC1967Upgrade {
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check that the execution is not being performed through a delegate call. This allows a function to be
|
||||
* callable on the implementing contract but not through proxies.
|
||||
*/
|
||||
modifier notDelegated() {
|
||||
require(address(this) == __self, "UUPSUpgradeable: must not be called through delegatecall");
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Implementation of the ERC1822 {proxiableUUID} function. This returns the storage slot used by the
|
||||
* implementation. It is used to validate that the this implementation remains valid after an upgrade.
|
||||
*
|
||||
* IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks
|
||||
* bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this
|
||||
* function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier.
|
||||
*/
|
||||
function proxiableUUID() external view virtual override notDelegated returns (bytes32) {
|
||||
return _IMPLEMENTATION_SLOT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Upgrade the implementation of the proxy to `newImplementation`.
|
||||
*
|
||||
@ -43,7 +65,7 @@ abstract contract UUPSUpgradeable is ERC1967Upgrade {
|
||||
*/
|
||||
function upgradeTo(address newImplementation) external virtual onlyProxy {
|
||||
_authorizeUpgrade(newImplementation);
|
||||
_upgradeToAndCallSecure(newImplementation, new bytes(0), false);
|
||||
_upgradeToAndCallUUPS(newImplementation, new bytes(0), false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,7 +78,7 @@ abstract contract UUPSUpgradeable is ERC1967Upgrade {
|
||||
*/
|
||||
function upgradeToAndCall(address newImplementation, bytes memory data) external payable virtual onlyProxy {
|
||||
_authorizeUpgrade(newImplementation);
|
||||
_upgradeToAndCallSecure(newImplementation, data, true);
|
||||
_upgradeToAndCallUUPS(newImplementation, data, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user