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

Simplify UUPSUpgradeable along the lines of ERC1822 #3021

Merged
merged 24 commits into from
Jan 13, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions contracts/proxy/ERC1822/IProxiable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.x.0 (proxy/ERC1822/IProxiable.sol)

pragma solidity ^0.8.0;

interface IProxiable {
Amxx marked this conversation as resolved.
Show resolved Hide resolved
function proxiableUUID() external view returns (bytes32);
}
29 changes: 8 additions & 21 deletions contracts/proxy/ERC1967/ERC1967Upgrade.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
pragma solidity ^0.8.2;

import "../beacon/IBeacon.sol";
import "../ERC1822/IProxiable.sol";
import "../../utils/Address.sol";
import "../../utils/StorageSlot.sol";

Expand Down Expand Up @@ -82,28 +83,14 @@ abstract contract ERC1967Upgrade {
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)
if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) {
frangio marked this conversation as resolved.
Show resolved Hide resolved
frangio marked this conversation as resolved.
Show resolved Hide resolved
_setImplementation(newImplementation);
} else {
require(
IProxiable(newImplementation).proxiableUUID() == _IMPLEMENTATION_SLOT,
"Invalid proxiableUUID value"
);
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);
_upgradeToAndCall(newImplementation, data, forceCall);
}
}

Expand Down
20 changes: 19 additions & 1 deletion contracts/proxy/utils/UUPSUpgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

pragma solidity ^0.8.0;

import "../ERC1822/IProxiable.sol";
import "../ERC1967/ERC1967Upgrade.sol";

/**
Expand All @@ -17,7 +18,7 @@ import "../ERC1967/ERC1967Upgrade.sol";
*
* _Available since v4.1._
*/
abstract contract UUPSUpgradeable is ERC1967Upgrade {
abstract contract UUPSUpgradeable is IProxiable, ERC1967Upgrade {
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment
address private immutable __self = address(this);

Expand All @@ -34,6 +35,23 @@ 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() {
frangio marked this conversation as resolved.
Show resolved Hide resolved
require(address(this) == __self, "Function must not be called through delegatecall");
Amxx marked this conversation as resolved.
Show resolved Hide resolved
_;
}

/**
* @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.
*/
function proxiableUUID() external view virtual override notDelegated returns (bytes32) {
frangio marked this conversation as resolved.
Show resolved Hide resolved
return _IMPLEMENTATION_SLOT;
}

/**
* @dev Upgrade the implementation of the proxy to `newImplementation`.
*
Expand Down
28 changes: 25 additions & 3 deletions test/proxy/utils/UUPSUpgradeable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const UUPSUpgradeableUnsafeMock = artifacts.require('UUPSUpgradeableUnsafeMock')
const UUPSUpgradeableBrokenMock = artifacts.require('UUPSUpgradeableBrokenMock');
const CountersImpl = artifacts.require('CountersImpl');

const Legacy = [ './legacy/UUPSUpgradeableMockV1' ];
Amxx marked this conversation as resolved.
Show resolved Hide resolved

contract('UUPSUpgradeable', function (accounts) {
before(async function () {
this.implInitial = await UUPSUpgradeableMock.new();
Expand Down Expand Up @@ -44,7 +46,7 @@ contract('UUPSUpgradeable', function (accounts) {
expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeUnsafe.address });
});

it('reject upgrade to broken upgradeable implementation', async function () {
it.skip('reject upgrade to broken upgradeable implementation (no longer supported)', async function () {
Amxx marked this conversation as resolved.
Show resolved Hide resolved
await expectRevert(
this.instance.upgradeTo(this.implUpgradeBroken.address),
'ERC1967Upgrade: upgrade breaks further upgrades',
Expand All @@ -55,7 +57,7 @@ contract('UUPSUpgradeable', function (accounts) {
it('reject upgrade to non uups implementation', async function () {
await expectRevert(
this.instance.upgradeTo(this.implUpgradeNonUUPS.address),
'Address: low-level delegate call failed',
'Transaction reverted: function selector was not recognized and there\'s no fallback function',
Amxx marked this conversation as resolved.
Show resolved Hide resolved
);
});

Expand All @@ -66,7 +68,27 @@ contract('UUPSUpgradeable', function (accounts) {
// infinite loop reverts when a nested call is out-of-gas
await expectRevert(
this.instance.upgradeTo(otherInstance.address),
'Address: low-level delegate call failed',
'Function must not be called through delegatecall',
);
});

describe('compatibility with legacy version of UUPSUpgradeable', function () {
// This could possibly be cleaner/simpler with ethers.js
for (const path of Legacy) {
it(`can upgrade from ${path}`, async function () {
const artefact = require(path);
// deploy legacy implementation
const legacy = new web3.eth.Contract(artefact.abi);
const { _address: implAddr } = await legacy.deploy({ data: artefact.bytecode }).send({ from: accounts[0] });

// deploy proxy
const { address: proxyAddr } = await ERC1967Proxy.new(implAddr, '0x');
const proxy = new web3.eth.Contract(artefact.abi, proxyAddr);

// perform upgrade
const receipt = await proxy.methods.upgradeTo(this.implInitial.address).send({ from: accounts[0] });
expectEvent(receipt, 'Upgraded', { implementation: this.implInitial.address });
});
Amxx marked this conversation as resolved.
Show resolved Hide resolved
}
});
});
Loading