Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make TransparentUpgradeableProxy deploy its ProxyAdmin and optimize proxy interfaces #4382

Merged
merged 48 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
a4215ca
Make TransparentUpgradeableProxy deploy its own ProxyAdmin
ernestognw Jun 23, 2023
9d39cf0
Merge branch 'master' into proxy-admin-optimizations
frangio Jun 30, 2023
2acbf84
revert changes to other proxies
frangio Jun 30, 2023
e48fa5a
change transparent proxy to have upgradeTo only
frangio Jun 30, 2023
d999991
embed ProxyAdmin creation in proxy constructor
frangio Jun 30, 2023
03c2cf5
remove gas snapshot
frangio Jun 30, 2023
f570cdc
revert test changes
frangio Jun 30, 2023
0303cc7
add back upgrade
frangio Jul 1, 2023
347fac0
remove extra space
frangio Jul 6, 2023
9de9ce8
inline behavior into test file
frangio Jul 6, 2023
34381f6
remove unnecessary argument
frangio Jul 6, 2023
bff3bc7
switch proxy to upgradeToAndCall
frangio Jul 7, 2023
d9e192e
Update contracts/proxy/transparent/TransparentUpgradeableProxy.sol
ernestognw Jul 7, 2023
2dd7c05
Lint
ernestognw Jul 7, 2023
62bbd7b
Remove unused import
ernestognw Jul 7, 2023
b71fc98
Fix tests
ernestognw Jul 8, 2023
2e0c396
add else branch
frangio Jul 8, 2023
ca99577
use forceCall = false
frangio Jul 8, 2023
909fe1d
use rlp from declared dependency
frangio Jul 8, 2023
d01100a
use hardhat-network-helpers
frangio Jul 8, 2023
b425a38
hardcode nonce for address calculation
frangio Jul 8, 2023
d4f5535
remove owner argument to createProxy function
frangio Jul 8, 2023
6e48769
update docs
frangio Jul 8, 2023
0f479c6
Update create.js
Amxx Jul 8, 2023
853dc83
Update account.js
Amxx Jul 8, 2023
864be51
Update create.js
Amxx Jul 8, 2023
9349f30
Update create.js
Amxx Jul 8, 2023
cd1ac92
Update GovernorTimelockCompound.test.js
Amxx Jul 8, 2023
c4d49a4
fix lint
Amxx Jul 8, 2023
6e95dac
change proxyadmin test descriptions
frangio Jul 8, 2023
4d3bb2f
use constant string
frangio Jul 8, 2023
c37cab0
remove forceCall and upgradeTo/AndCall distinction
frangio Jul 8, 2023
5c77c03
remove unused return value from transparent proxy dispatch
frangio Jul 8, 2023
ff6a70e
lint
frangio Jul 8, 2023
223d840
remove unused import
frangio Jul 8, 2023
66a3ebf
remove unused error
frangio Jul 8, 2023
be31da2
Merge branch 'master' into proxy-admin-optimizations
frangio Jul 9, 2023
802def2
Moaaar review
ernestognw Jul 9, 2023
a22e182
don't pass empty opts to constructor removes the need for {from:...} …
Amxx Jul 9, 2023
dc040f1
Improve docs and apply suggestions
ernestognw Jul 12, 2023
4b9ec42
Improve changeset
ernestognw Jul 12, 2023
22698a6
Remove unnecessary slohint annotations
ernestognw Jul 13, 2023
60123c7
Merge branch 'master' into proxy-admin-optimizations
ernestognw Jul 13, 2023
9f18301
Add ERC1967Utils tests
ernestognw Jul 13, 2023
75ea1de
Add UpgradeableBeaconMock
ernestognw Jul 13, 2023
08ad9b5
remove unused import
frangio Jul 13, 2023
7e5c33b
Use events for checking delegatecalls
ernestognw Jul 13, 2023
703659e
Use events for checking delegatecalls
ernestognw Jul 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/empty-taxis-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': major
---

`UUPSUpgradeable`, `TransparentUpgradeableProxy` and `ProxyAdmin`: Removed `upgradeTo` and `upgrade` functions, and made `upgradeToAndCall` and `upgradeAndCall` ignore the data argument if it is empty. It is no longer possible to invoke the receive function (or send value with empty data) along with an upgrade.
5 changes: 5 additions & 0 deletions .changeset/tender-shirts-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': major
---

`BeaconProxy`: Reject value in initialization unless a payable function is explicitly invoked.
12 changes: 12 additions & 0 deletions contracts/mocks/UpgreadeableBeaconMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {IBeacon} from "../proxy/beacon/IBeacon.sol";

contract UpgradeableBeaconMock is IBeacon {
address public implementation;

constructor(address impl) {
implementation = impl;
}
}
2 changes: 1 addition & 1 deletion contracts/mocks/proxy/ClashingImplementation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pragma solidity ^0.8.19;
contract ClashingImplementation {
event ClashingImplementationCall();

function upgradeTo(address) external payable {
function upgradeToAndCall(address, bytes calldata) external payable {
emit ClashingImplementationCall();
}

Expand Down
6 changes: 1 addition & 5 deletions contracts/mocks/proxy/UUPSUpgradeableMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,8 @@ contract UUPSUpgradeableMock is NonUpgradeableMock, UUPSUpgradeable {
}

contract UUPSUpgradeableUnsafeMock is UUPSUpgradeableMock {
function upgradeTo(address newImplementation) public override {
ERC1967Utils.upgradeToAndCall(newImplementation, bytes(""), false);
}

function upgradeToAndCall(address newImplementation, bytes memory data) public payable override {
ERC1967Utils.upgradeToAndCall(newImplementation, data, false);
ERC1967Utils.upgradeToAndCall(newImplementation, data);
}
}

Expand Down
6 changes: 5 additions & 1 deletion contracts/proxy/ERC1967/ERC1967Proxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ contract ERC1967Proxy is Proxy {
*
* If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded
* function call, and allows initializing the storage of the proxy like a Solidity constructor.
*
* Requirements:
*
* - If `data` is empty, `msg.value` must be zero.
*/
constructor(address _logic, bytes memory _data) payable {
ERC1967Utils.upgradeToAndCall(_logic, _data, false);
ERC1967Utils.upgradeToAndCall(_logic, _data);
}

/**
Expand Down
43 changes: 29 additions & 14 deletions contracts/proxy/ERC1967/ERC1967Utils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ library ERC1967Utils {
*/
error ERC1967InvalidBeacon(address beacon);

/**
* @dev An upgrade function sees `msg.value > 0` that may be lost.
*/
error ERC1967NonPayable();

/**
* @dev Returns the current implementation address.
*/
Expand All @@ -70,24 +75,20 @@ library ERC1967Utils {
}

/**
* @dev Perform implementation upgrade
* @dev Performs implementation upgrade with additional setup call if data is nonempty.
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
* This function is payable only if the setup call is performed, otherwise `msg.value` is rejected
* to avoid stuck value in the contract.
*
* Emits an {IERC1967-Upgraded} event.
*/
function upgradeTo(address newImplementation) internal {
function upgradeToAndCall(address newImplementation, bytes memory data) internal {
_setImplementation(newImplementation);
emit Upgraded(newImplementation);
}

/**
* @dev Perform implementation upgrade with additional setup call.
*
* Emits an {IERC1967-Upgraded} event.
*/
function upgradeToAndCall(address newImplementation, bytes memory data, bool forceCall) internal {
upgradeTo(newImplementation);
if (data.length > 0 || forceCall) {
if (data.length > 0) {
Address.functionDelegateCall(newImplementation, data);
} else {
_checkNonPayable();
}
}

Expand Down Expand Up @@ -161,19 +162,33 @@ library ERC1967Utils {
}

/**
* @dev Change the beacon and trigger a setup call.
* @dev Change the beacon and trigger a setup call if data is nonempty.
* This function is payable only if the setup call is performed, otherwise `msg.value` is rejected
* to avoid stuck value in the contract.
*
* Emits an {IERC1967-BeaconUpgraded} event.
*
* CAUTION: Invoking this function has no effect on an instance of {BeaconProxy} since v5, since
* it uses an immutable beacon without looking at the value of the ERC-1967 beacon slot for
* efficiency.
*/
function upgradeBeaconToAndCall(address newBeacon, bytes memory data, bool forceCall) internal {
function upgradeBeaconToAndCall(address newBeacon, bytes memory data) internal {
_setBeacon(newBeacon);
emit BeaconUpgraded(newBeacon);
if (data.length > 0 || forceCall) {

if (data.length > 0) {
Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);
} else {
_checkNonPayable();
}
}

/**
* @dev Reverts if `msg.value` is not zero.
*/
function _checkNonPayable() private {
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
if (msg.value > 0) {
revert ERC1967NonPayable();
}
}
}
3 changes: 2 additions & 1 deletion contracts/proxy/beacon/BeaconProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ contract BeaconProxy is Proxy {
* Requirements:
*
* - `beacon` must be a contract with the interface {IBeacon}.
* - If `data` is empty, `msg.value` must be zero.
*/
constructor(address beacon, bytes memory data) payable {
ERC1967Utils.upgradeBeaconToAndCall(beacon, data, false);
ERC1967Utils.upgradeBeaconToAndCall(beacon, data);
_beacon = beacon;
}

Expand Down
24 changes: 12 additions & 12 deletions contracts/proxy/transparent/ProxyAdmin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,28 @@ import {Ownable} from "../../access/Ownable.sol";
*/
contract ProxyAdmin is Ownable {
/**
* @dev Sets the initial owner who can perform upgrades.
* @dev The version of the upgrade interface of the contract. If this getter is missing, both `upgrade(address)`
* and `upgradeAndCall(address,bytes)` are present, and `upgradeTo` must be used if no function should be called,
* while `upgradeAndCall` will invoke the `receive` function if the second argument is the empty byte string.
* If the getter returns `"5.0.0"`, only `upgradeAndCall(address,bytes)` is present, and the second argument must
* be the empty byte string if no function should be called, being impossible to invoke the `receive` function
* during an upgrade.
*/
constructor(address initialOwner) Ownable(initialOwner) {}
string public constant UPGRADE_INTERFACE_VERSION = "5.0.0";

/**
* @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
* @dev Sets the initial owner who can perform upgrades.
*/
function upgrade(ITransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner {
proxy.upgradeTo(implementation);
}
constructor(address initialOwner) Ownable(initialOwner) {}

/**
* @dev Upgrades `proxy` to `implementation` and calls a function on the new implementation. See
* {TransparentUpgradeableProxy-upgradeToAndCall}.
* @dev Upgrades `proxy` to `implementation` and calls a function on the new implementation.
* See {TransparentUpgradeableProxy-_dispatchUpgradeToAndCall}.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
* - If `data` is empty, `msg.value` must be zero.
*/
function upgradeAndCall(
ITransparentUpgradeableProxy proxy,
Expand Down
97 changes: 31 additions & 66 deletions contracts/proxy/transparent/TransparentUpgradeableProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,43 @@ pragma solidity ^0.8.19;
import {ERC1967Utils} from "../ERC1967/ERC1967Utils.sol";
import {ERC1967Proxy} from "../ERC1967/ERC1967Proxy.sol";
import {IERC1967} from "../../interfaces/IERC1967.sol";
import {ProxyAdmin} from "./ProxyAdmin.sol";

/**
* @dev Interface for {TransparentUpgradeableProxy}. In order to implement transparency, {TransparentUpgradeableProxy}
* does not implement this interface directly, and some of its functions are implemented by an internal dispatch
* does not implement this interface directly, and its upgradeability mechanism is implemented by an internal dispatch
* mechanism. The compiler is unaware that these functions are implemented by {TransparentUpgradeableProxy} and will not
* include them in the ABI so this interface must be used to interact with it.
*/
interface ITransparentUpgradeableProxy is IERC1967 {
function upgradeTo(address) external;

function upgradeToAndCall(address, bytes memory) external payable;
function upgradeToAndCall(address, bytes calldata) external payable;
}

/**
* @dev This contract implements a proxy that is upgradeable by an immutable admin.
* @dev This contract implements a proxy that is upgradeable through an associated {ProxyAdmin} instance.
*
* To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector
* clashing], which can potentially be used in an attack, this contract uses the
* https://blog.openzeppelin.com/the-transparent-proxy-pattern/[transparent proxy pattern]. This pattern implies two
* things that go hand in hand:
*
* 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if
* that call matches one of the admin functions exposed by the proxy itself.
* 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the
* that call matches the {ITransparentUpgradeableProxy-upgradeToAndCall} function exposed by the proxy itself.
* 2. If the admin calls the proxy, it can call the `upgradeToAndCall` function but any other call won't be forwarded to the
* implementation. If the admin tries to call a function on the implementation it will fail with an error indicating the
* proxy admin cannot fallback to the target implementation.
*
* These properties mean that the admin account can only be used for upgrading the proxy, so it's best if it's a dedicated
* account that is not used for anything else. This will avoid headaches due to sudden errors when trying to call a function
* from the proxy implementation.
*
* Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way,
* you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy, which extends from the
* {Ownable} contract to allow for changing the proxy's admin owner.
* from the proxy implementation. For this reason, the proxy deploys an instance of {ProxyAdmin} and allows upgrades
* only if they come through it.
* You should think of the `ProxyAdmin` instance as the administrative interface of the proxy, including the ability to
* change who can trigger upgrades by transferring ownership.
*
* NOTE: The real interface of this proxy is that defined in `ITransparentUpgradeableProxy`. This contract does not
* inherit from that interface, and instead the admin functions are implicitly implemented using a custom dispatch
* mechanism in `_fallback`. Consequently, the compiler will not produce an ABI for this contract. This is necessary to
* fully implement transparency without decoding reverts caused by selector clashes between the proxy and the
* inherit from that interface, and instead `upgradeToAndCall` is implicitly implemented using a custom dispatch mechanism
* in `_fallback`. Consequently, the compiler will not produce an ABI for this contract. This is necessary to fully
* implement transparency without decoding reverts caused by selector clashes between the proxy and the
* implementation.
*
* IMPORTANT: This contract avoids unnecessary storage reads by setting the admin only during construction as an immutable variable,
Expand All @@ -55,10 +53,10 @@ interface ITransparentUpgradeableProxy is IERC1967 {
* WARNING: It is not recommended to extend this contract to add additional external functions. If you do so, the compiler
* will not check that there are no selector conflicts, due to the note above. A selector clash between any new function
* and the functions declared in {ITransparentUpgradeableProxy} will be resolved in favor of the new one. This could
* render the admin operations inaccessible, which could prevent upgradeability. Transparency may also be compromised.
* render the `upgradeToAndCall` function inaccessible, preventing upgradeability and compromising transparency.
*/
contract TransparentUpgradeableProxy is ERC1967Proxy {
// An immutable address for the admin avoid unnecessary SLOADs before each call
// An immutable address for the admin to avoid unnecessary SLOADs before each call
// at the expense of removing the ability to change the admin once it's set.
// This is acceptable if the admin is always a ProxyAdmin instance or similar contract
// with its own ability to transfer the permissions to another account.
Expand All @@ -70,73 +68,40 @@ contract TransparentUpgradeableProxy is ERC1967Proxy {
error ProxyDeniedAdminAccess();

/**
* @dev msg.value is not 0.
*/
error ProxyNonPayableFunction();

/**
* @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and
* optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}.
* @dev Initializes an upgradeable proxy managed by an instance of a {ProxyAdmin} with an `initialOwner`,
* backed by the implementation at `_logic`, and optionally initialized with `_data` as explained in
* {ERC1967Proxy-constructor}.
*/
constructor(address _logic, address admin_, bytes memory _data) payable ERC1967Proxy(_logic, _data) {
_admin = admin_;
constructor(address _logic, address initialOwner, bytes memory _data) payable ERC1967Proxy(_logic, _data) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey i just stumbled over this change when reviewing v5 and was wondering if it wouldn't make sense to have an overload with ProxyAdmin _admin instead of address initialOwner.

This way one could easily reuse a single ProxyAdmin for multiple contracts.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way one could easily reuse a single ProxyAdmin for multiple contracts.

This used ti be the case in v4. We decided to change that.

In v4, the admin was stored in storage, which means that any call going trough the proxy would have to load that value, costing 2100 gas.

function _fallback() internal virtual override {
        if (msg.sender == _admin) {
            ...

We wanted to reduce that gas cost, and so we decided to make the admin immutable. That means that it is not possible for a proxy to change which admin it uses. You could change the value stored in the ERC-1967 slot, but that would have no effect.

That means that if multiple proxy use the same ProxyAdmin, then you can separate the ownership. You may be able to change who owns the ProxyAdmin, but every proxy that point to it will share the same owner, forever.

That is why we decided to make the TransparentUpgradeableProxy <> ProxyAdmin a 1-to-1 relationship.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with @Amxx.

We agreed that it's better to reduce the SLOAD cost in the proxy than allowing to reuse ProxyAdmins. The tradeoff is fine because this not only increases deployment costs but reduces every proxy interaction cost. Consider regular proxy interactions are more frequent (potentially millions of times) than admin interactions.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just stumbled upon this while deploying a proxy using v5. Being used to the v4 behavior, I wrongly passed in a proxy admin making the deployment unusable. As far as I could tell, this change is not documented in any changelog. I think that it would be good to add this to the v5 changelog.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sharing @danhper! The change is documented on both OZ upgrades plugins and OZ Contracts (see Proxy section). Though I know it's counterintuitive, we strongly advice users to be careful about major releases.

We'd be happy to hear suggestions on how to improve the way we communicate these changes.

_admin = address(new ProxyAdmin(initialOwner));
// Set the storage value and emit an event for ERC-1967 compatibility
ERC1967Utils.changeAdmin(admin_);
ERC1967Utils.changeAdmin(_admin);
}

/**
* @dev If caller is the admin process the call internally, otherwise transparently fallback to the proxy behavior
* @dev If caller is the admin process the call internally, otherwise transparently fallback to the proxy behavior.
*/
function _fallback() internal virtual override {
if (msg.sender == _admin) {
bytes memory ret;
bytes4 selector = msg.sig;
if (selector == ITransparentUpgradeableProxy.upgradeTo.selector) {
ret = _dispatchUpgradeTo();
} else if (selector == ITransparentUpgradeableProxy.upgradeToAndCall.selector) {
ret = _dispatchUpgradeToAndCall();
if (msg.sig == ITransparentUpgradeableProxy.upgradeToAndCall.selector) {
_dispatchUpgradeToAndCall();
} else {
revert ProxyDeniedAdminAccess();
}
assembly {
return(add(ret, 0x20), mload(ret))
}
} else {
super._fallback();
}
}

/**
* @dev Upgrade the implementation of the proxy.
*/
function _dispatchUpgradeTo() private returns (bytes memory) {
_requireZeroValue();

address newImplementation = abi.decode(msg.data[4:], (address));
ERC1967Utils.upgradeToAndCall(newImplementation, bytes(""), false);

return "";
}

/**
* @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified
* by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the
* proxied contract.
* @dev Upgrade the implementation of the proxy. See {ERC1967Utils-upgradeToAndCall}.
*
* Requirements:
*
* - If `data` is empty, `msg.value` must be zero.
*/
function _dispatchUpgradeToAndCall() private returns (bytes memory) {
function _dispatchUpgradeToAndCall() private {
(address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes));
ERC1967Utils.upgradeToAndCall(newImplementation, data, true);

return "";
}

/**
* @dev To keep this contract fully transparent, the fallback is payable. This helper is here to enforce
* non-payability of function implemented through dispatchers while still allowing value to pass through.
*/
function _requireZeroValue() private {
if (msg.value != 0) {
revert ProxyNonPayableFunction();
}
ERC1967Utils.upgradeToAndCall(newImplementation, data);
}
}
Loading