diff --git a/docs/modules/ROOT/pages/api-hardhat-upgrades.adoc b/docs/modules/ROOT/pages/api-hardhat-upgrades.adoc index 408720341..a1b8b6e70 100644 --- a/docs/modules/ROOT/pages/api-hardhat-upgrades.adoc +++ b/docs/modules/ROOT/pages/api-hardhat-upgrades.adoc @@ -616,6 +616,8 @@ async function changeProxyAdmin( Changes the admin for a specific proxy. +NOTE: This function is not supported with admins or proxies from OpenZeppelin Contracts 5.x. + *Parameters:* * `proxyAddress` - the address of the proxy to change. diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 4784c37ab..887a2205e 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Support new upgrade interface in OpenZeppelin Contracts 5.0. ([883](https://github.com/OpenZeppelin/openzeppelin-upgrades/pull/883)) - Add validations for namespaced storage layout. ([#876](https://github.com/OpenZeppelin/openzeppelin-upgrades/pull/876)) - Deprecate low-level API. Use [CLI or high-level API](https://docs.openzeppelin.com/upgrades-plugins/1.x/api-core) instead. diff --git a/packages/core/package.json b/packages/core/package.json index fdbbe6d59..d044aac43 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/upgrades-core", - "version": "1.29.0", + "version": "1.30.0", "description": "", "repository": "https://github.com/OpenZeppelin/openzeppelin-upgrades/tree/master/packages/core", "license": "MIT", diff --git a/packages/core/src/call-optional-signature.ts b/packages/core/src/call-optional-signature.ts new file mode 100644 index 000000000..04121cb62 --- /dev/null +++ b/packages/core/src/call-optional-signature.ts @@ -0,0 +1,20 @@ +import { keccak256 } from 'ethereumjs-util'; +import { call, EthereumProvider } from './provider'; + +export async function callOptionalSignature(provider: EthereumProvider, address: string, signature: string) { + const data = '0x' + keccak256(Buffer.from(signature)).toString('hex').slice(0, 8); + try { + return await call(provider, address, data); + } catch (e: any) { + if ( + e.message.includes('function selector was not recognized') || + e.message.includes('invalid opcode') || + e.message.includes('revert') || + e.message.includes('execution error') + ) { + return undefined; + } else { + throw e; + } + } +} diff --git a/packages/core/src/impl-address.ts b/packages/core/src/impl-address.ts index 426570702..8c2b2291f 100644 --- a/packages/core/src/impl-address.ts +++ b/packages/core/src/impl-address.ts @@ -1,12 +1,11 @@ -import { keccak256 } from 'ethereumjs-util'; import { - call, EIP1967BeaconNotFound, EIP1967ImplementationNotFound, getBeaconAddress, getImplementationAddress, UpgradesError, } from '.'; +import { callOptionalSignature } from './call-optional-signature'; import { EthereumProvider } from './provider'; import { parseAddress } from './utils/address'; @@ -24,27 +23,17 @@ export async function getImplementationAddressFromBeacon( provider: EthereumProvider, beaconAddress: string, ): Promise { - const implementationFunction = '0x' + keccak256(Buffer.from('implementation()')).toString('hex').slice(0, 8); - let result: string | undefined; - try { - const implAddress = await call(provider, beaconAddress, implementationFunction); - result = parseAddress(implAddress); - } catch (e: any) { - if ( - !( - e.message.includes('function selector was not recognized') || - e.message.includes('invalid opcode') || - e.message.includes('revert') || - e.message.includes('execution error') - ) - ) { - throw e; - } // otherwise fall through with no result + const impl = await callOptionalSignature(provider, beaconAddress, 'implementation()'); + let parsedImplAddress; + if (impl !== undefined) { + parsedImplAddress = parseAddress(impl); } - if (result === undefined) { + + if (parsedImplAddress === undefined) { throw new InvalidBeacon(`Contract at ${beaconAddress} doesn't look like a beacon`); + } else { + return parsedImplAddress; } - return result; } /** diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e8fe2ab06..a8c4533d2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -58,4 +58,5 @@ export { export { ValidateUpgradeSafetyOptions, validateUpgradeSafety, ProjectReport, ReferenceContractNotFound } from './cli'; +export { getUpgradeInterfaceVersion } from './upgrade-interface-version'; export { makeNamespacedInput } from './utils/make-namespaced'; diff --git a/packages/core/src/upgrade-interface-version.test.ts b/packages/core/src/upgrade-interface-version.test.ts new file mode 100644 index 000000000..a0986dec2 --- /dev/null +++ b/packages/core/src/upgrade-interface-version.test.ts @@ -0,0 +1,37 @@ +import test from 'ava'; +import { EthereumProvider } from './provider'; +import { getUpgradeInterfaceVersion } from './upgrade-interface-version'; + +const hash = '0x1234'; + +function makeProviderReturning(result: unknown): EthereumProvider { + return { send: (_method: string, _params: unknown[]) => Promise.resolve(result) } as EthereumProvider; +} + +function makeProviderError(msg: string): EthereumProvider { + return { + send: (_method: string, _params: unknown[]) => { + throw new Error(msg); + }, + } as EthereumProvider; +} + +test('getUpgradeInterfaceVersion returns version', async t => { + // abi encoding of '5.0.0' + const provider = makeProviderReturning( + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005352e302e30000000000000000000000000000000000000000000000000000000', + ); + t.is(await getUpgradeInterfaceVersion(provider, hash), '5.0.0'); +}); + +test('getUpgradeInterfaceVersion throws unrelated error', async t => { + const provider = makeProviderError('unrelated error'); + await t.throwsAsync(() => getUpgradeInterfaceVersion(provider, hash), { message: 'unrelated error' }); +}); + +test('getUpgradeInterfaceVersion returns undefined for invalid selector', async t => { + const provider = makeProviderError( + `Transaction reverted: function selector was not recognized and there's no fallback function`, + ); + t.is(await getUpgradeInterfaceVersion(provider, hash), undefined); +}); diff --git a/packages/core/src/upgrade-interface-version.ts b/packages/core/src/upgrade-interface-version.ts new file mode 100644 index 000000000..8667c397a --- /dev/null +++ b/packages/core/src/upgrade-interface-version.ts @@ -0,0 +1,27 @@ +import { callOptionalSignature } from './call-optional-signature'; +import { EthereumProvider } from './provider'; + +export async function getUpgradeInterfaceVersion( + provider: EthereumProvider, + address: string, +): Promise { + const encodedVersion = await callOptionalSignature(provider, address, 'UPGRADE_INTERFACE_VERSION()'); + if (encodedVersion !== undefined) { + // Encoded string + const buf = Buffer.from(encodedVersion.replace(/^0x/, ''), 'hex'); + + // The first 32 bytes represent the offset, which should be 32 for a string + const offset = parseInt(buf.slice(0, 32).toString('hex'), 16); + if (offset !== 32) { + throw new Error(`Unexpected type for UPGRADE_INTERFACE_VERSION at address ${address}. Expected a string`); + } + + // The next 32 bytes represent the length of the string + const length = parseInt(buf.slice(32, 64).toString('hex'), 16); + + // The rest is the string itself + return buf.slice(64, 64 + length).toString('utf8'); + } else { + return undefined; + } +} diff --git a/packages/core/src/validate/query.ts b/packages/core/src/validate/query.ts index 62f4706d1..6393eed7a 100644 --- a/packages/core/src/validate/query.ts +++ b/packages/core/src/validate/query.ts @@ -194,7 +194,7 @@ export function isUpgradeSafe(data: ValidationData, version: Version): boolean { export function inferUUPS(runValidation: ValidationRunData, fullContractName: string): boolean { const methods = getAllMethods(runValidation, fullContractName); - return methods.includes(upgradeToSignature); + return methods.includes(upgradeToSignature) || methods.includes(upgradeToAndCallSignature); } export function inferProxyKind(data: ValidationData, version: Version): ProxyDeployment['kind'] { diff --git a/packages/plugin-hardhat/CHANGELOG.md b/packages/plugin-hardhat/CHANGELOG.md index 22a7819ea..f03f4f74b 100644 --- a/packages/plugin-hardhat/CHANGELOG.md +++ b/packages/plugin-hardhat/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Support new upgrade interface in OpenZeppelin Contracts 5.0. ([#883](https://github.com/OpenZeppelin/openzeppelin-upgrades/pull/883)) +- Support importing and upgrading 5.0 proxies. + - **Note**: Deploying 5.0 proxies is not supported yet. - Add validations for namespaced storage layout. ([#876](https://github.com/OpenZeppelin/openzeppelin-upgrades/pull/876)) ## 2.2.1 (2023-08-18) diff --git a/packages/plugin-hardhat/contracts/Greeter50Proxiable.sol b/packages/plugin-hardhat/contracts/Greeter50Proxiable.sol new file mode 100644 index 000000000..6300f68da --- /dev/null +++ b/packages/plugin-hardhat/contracts/Greeter50Proxiable.sol @@ -0,0 +1,6 @@ +pragma solidity >= 0.4.22 <0.8.0; + +import "./Greeter.sol"; +import "./utils/Proxiable50.sol"; + +contract Greeter50Proxiable is Greeter, Proxiable50 {} diff --git a/packages/plugin-hardhat/contracts/Greeter50V2Proxiable.sol b/packages/plugin-hardhat/contracts/Greeter50V2Proxiable.sol new file mode 100644 index 000000000..e713b381f --- /dev/null +++ b/packages/plugin-hardhat/contracts/Greeter50V2Proxiable.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.1; + +import "./GreeterV2.sol"; +import "./utils/Proxiable50.sol"; + +contract Greeter50V2Proxiable is GreeterV2, Proxiable50 {} diff --git a/packages/plugin-hardhat/contracts/Greeter50V3Proxiable.sol b/packages/plugin-hardhat/contracts/Greeter50V3Proxiable.sol new file mode 100644 index 000000000..efb648426 --- /dev/null +++ b/packages/plugin-hardhat/contracts/Greeter50V3Proxiable.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.1; + +import "./GreeterV3.sol"; +import "./utils/Proxiable50.sol"; + +contract Greeter50V3Proxiable is GreeterV3, Proxiable50 {} diff --git a/packages/plugin-hardhat/contracts/Import50.sol b/packages/plugin-hardhat/contracts/Import50.sol new file mode 100644 index 000000000..fe639347c --- /dev/null +++ b/packages/plugin-hardhat/contracts/Import50.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-5.0/proxy/beacon/BeaconProxy.sol"; +import "@openzeppelin/contracts-5.0/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts-5.0/proxy/ERC1967/ERC1967Proxy.sol"; +import "@openzeppelin/contracts-5.0/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts-5.0/proxy/transparent/ProxyAdmin.sol"; \ No newline at end of file diff --git a/packages/plugin-hardhat/contracts/utils/Proxiable50.sol b/packages/plugin-hardhat/contracts/utils/Proxiable50.sol new file mode 100644 index 000000000..e6162df88 --- /dev/null +++ b/packages/plugin-hardhat/contracts/utils/Proxiable50.sol @@ -0,0 +1,46 @@ +pragma solidity >= 0.4.22 <0.8.0; + +// This contract is for testing only, it is not safe for use in production. + +contract Proxiable50 { + bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + string public constant UPGRADE_INTERFACE_VERSION = "5.0.0"; + + function upgradeToAndCall(address newImplementation, bytes calldata data) external { + _setImplementation(newImplementation); + if (data.length > 0) { + /** + * Using address(this).call is dangerous as the call can impersonate the proxy being upgraded. + * a better option is to use a delegate call with an oz-upgrades-unsafe-allow, but this is not + * supported by the early version of solidity used here. + * + * /// @custom:oz-upgrades-unsafe-allow delegatecall + * (bool success, ) = newImplementation.delegatecall(data); + * + * Note that using delegate call can make your implementation contract vulnerable if this function + * is not protected with the `onlyProxy` modifier. Again, This contract is for testing only, it is + * not safe for use in production. Instead, use the `UUPSUpgradeable` contract available in + * @openzeppelin/contracts-upgradeable + */ + (bool success, ) = address(this).call(data); + require(success, "upgrade call reverted"); + } else { + _checkNonPayable(); + } + } + + function _checkNonPayable() private { + if (msg.value > 0) { + revert('non-payable upgrade call'); + } + } + + function _setImplementation(address newImplementation) private { + bytes32 slot = _IMPLEMENTATION_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, newImplementation) + } + } +} diff --git a/packages/plugin-hardhat/package.json b/packages/plugin-hardhat/package.json index 61b40a6d9..360c19e05 100644 --- a/packages/plugin-hardhat/package.json +++ b/packages/plugin-hardhat/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/hardhat-upgrades", - "version": "2.2.1", + "version": "2.3.0", "description": "", "repository": "https://github.com/OpenZeppelin/openzeppelin-upgrades/tree/master/packages/plugin-hardhat", "license": "MIT", @@ -25,6 +25,7 @@ "@nomicfoundation/hardhat-verify": "^1.1.0", "@openzeppelin/contracts": "4.8.3", "@openzeppelin/contracts-upgradeable": "4.8.3", + "@openzeppelin/contracts-5.0": "npm:@openzeppelin/contracts@^5.0.0-rc.0", "@types/mocha": "^7.0.2", "ava": "^5.0.0", "fgbg": "^0.1.4", @@ -38,7 +39,7 @@ "@openzeppelin/defender-admin-client": "^1.48.0", "@openzeppelin/defender-base-client": "^1.48.0", "@openzeppelin/platform-deploy-client": "^0.10.0", - "@openzeppelin/upgrades-core": "^1.27.0", + "@openzeppelin/upgrades-core": "^1.30.0", "chalk": "^4.1.0", "debug": "^4.1.1", "ethereumjs-util": "^7.1.5", diff --git a/packages/plugin-hardhat/src/upgrade-proxy.ts b/packages/plugin-hardhat/src/upgrade-proxy.ts index 4084a299d..cc40fc539 100644 --- a/packages/plugin-hardhat/src/upgrade-proxy.ts +++ b/packages/plugin-hardhat/src/upgrade-proxy.ts @@ -1,7 +1,7 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import type { ethers, ContractFactory, Contract, Signer } from 'ethers'; -import { getAdminAddress, getCode, isEmptySlot } from '@openzeppelin/upgrades-core'; +import { getAdminAddress, getCode, getUpgradeInterfaceVersion, isEmptySlot } from '@openzeppelin/upgrades-core'; import { UpgradeProxyOptions, @@ -54,17 +54,39 @@ export function makeUpgradeProxy(hre: HardhatRuntimeEnvironment, defenderModule: const ITransparentUpgradeableProxyFactory = await getITransparentUpgradeableProxyFactory(hre, signer); const proxy = attach(ITransparentUpgradeableProxyFactory, proxyAddress); - return (nextImpl, call) => - call ? proxy.upgradeToAndCall(nextImpl, call, ...overrides) : proxy.upgradeTo(nextImpl, ...overrides); + const upgradeInterfaceVersion = await getUpgradeInterfaceVersion(provider, proxyAddress); + + return (nextImpl, call) => { + if (upgradeInterfaceVersion === undefined) { + return call ? proxy.upgradeToAndCall(nextImpl, call, ...overrides) : proxy.upgradeTo(nextImpl, ...overrides); + } else if (upgradeInterfaceVersion === '5.0.0') { + return proxy.upgradeToAndCall(nextImpl, call ?? '0x', ...overrides); + } else { + throw new Error( + `Unknown UPGRADE_INTERFACE_VERSION ${upgradeInterfaceVersion} for proxy at ${proxyAddress}. Expected 5.0.0`, + ); + } + }; } else { // Admin contract: redirect upgrade call through it const AdminFactory = await getProxyAdminFactory(hre, signer); const admin = attach(AdminFactory, adminAddress); - return (nextImpl, call) => - call - ? admin.upgradeAndCall(proxyAddress, nextImpl, call, ...overrides) - : admin.upgrade(proxyAddress, nextImpl, ...overrides); + const upgradeInterfaceVersion = await getUpgradeInterfaceVersion(provider, adminAddress); + + return (nextImpl, call) => { + if (upgradeInterfaceVersion === undefined) { + return call + ? admin.upgradeAndCall(proxyAddress, nextImpl, call, ...overrides) + : admin.upgrade(proxyAddress, nextImpl, ...overrides); + } else if (upgradeInterfaceVersion === '5.0.0') { + return admin.upgradeAndCall(proxyAddress, nextImpl, call ?? '0x', ...overrides); + } else { + throw new Error( + `Unknown UPGRADE_INTERFACE_VERSION ${upgradeInterfaceVersion} for proxy admin at ${adminAddress}. Expected 5.0.0`, + ); + } + }; } } } diff --git a/packages/plugin-hardhat/test/import-50.js b/packages/plugin-hardhat/test/import-50.js new file mode 100644 index 000000000..899fad9bd --- /dev/null +++ b/packages/plugin-hardhat/test/import-50.js @@ -0,0 +1,369 @@ +const test = require('ava'); + +const { ethers, upgrades } = require('hardhat'); + +const ProxyAdmin = require('../artifacts/@openzeppelin/contracts-5.0/proxy/transparent/ProxyAdmin.sol/ProxyAdmin.json'); +const TransparentUpgradableProxy = require('../artifacts/@openzeppelin/contracts-5.0/proxy/transparent/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json'); + +const ERC1967Proxy = require('../artifacts/@openzeppelin/contracts-5.0/proxy/ERC1967/ERC1967Proxy.sol/ERC1967Proxy.json'); + +const BeaconProxy = require('../artifacts/@openzeppelin/contracts-5.0/proxy/beacon/BeaconProxy.sol/BeaconProxy.json'); +const UpgradableBeacon = require('../artifacts/@openzeppelin/contracts-5.0/proxy/beacon/UpgradeableBeacon.sol/UpgradeableBeacon.json'); + +test.before(async t => { + t.context.Greeter = await ethers.getContractFactory('Greeter'); + t.context.GreeterV2 = await ethers.getContractFactory('GreeterV2'); + t.context.GreeterV3 = await ethers.getContractFactory('GreeterV3'); + t.context.GreeterProxiable = await ethers.getContractFactory('Greeter50Proxiable'); + t.context.GreeterV2Proxiable = await ethers.getContractFactory('Greeter50V2Proxiable'); + t.context.GreeterV3Proxiable = await ethers.getContractFactory('GreeterV3Proxiable'); + t.context.CustomProxy = await ethers.getContractFactory('CustomProxy'); + t.context.CustomProxyWithAdmin = await ethers.getContractFactory('CustomProxyWithAdmin'); + + t.context.ProxyAdmin = await ethers.getContractFactory(ProxyAdmin.abi, ProxyAdmin.bytecode); + t.context.TransparentUpgradableProxy = await ethers.getContractFactory( + TransparentUpgradableProxy.abi, + TransparentUpgradableProxy.bytecode, + ); + + t.context.ERC1967Proxy = await ethers.getContractFactory(ERC1967Proxy.abi, ERC1967Proxy.bytecode); + + t.context.BeaconProxy = await ethers.getContractFactory(BeaconProxy.abi, BeaconProxy.bytecode); + t.context.UpgradableBeacon = await ethers.getContractFactory(UpgradableBeacon.abi, UpgradableBeacon.bytecode); +}); + +function getInitializerData(contractInterface, args) { + const initializer = 'initialize'; + const fragment = contractInterface.getFunction(initializer); + return contractInterface.encodeFunctionData(fragment, args); +} + +const REQUESTED_UPGRADE_WRONG_KIND = 'Requested an upgrade of kind uups but proxy is transparent'; + +test('implementation happy path', async t => { + const { GreeterProxiable } = t.context; + + const impl = await GreeterProxiable.deploy(); + await impl.waitForDeployment(); + + const contract = await upgrades.forceImport(await impl.getAddress(), GreeterProxiable); + t.is(await impl.getAddress(), await contract.getAddress()); + t.is('', await contract.greet()); + + const greeter = await upgrades.deployProxy(GreeterProxiable, ['Hello, Hardhat!'], { + useDeployedImplementation: true, + }); + t.is(await greeter.greet(), 'Hello, Hardhat!'); +}); + +test('no contract', async t => { + const { GreeterProxiable } = t.context; + + const e = await t.throwsAsync(() => + upgrades.forceImport('0x0000000000000000000000000000000000000001', GreeterProxiable), + ); + t.true(e.message.startsWith('No contract at address 0x0000000000000000000000000000000000000001'), e.message); +}); + +test('transparent happy path', async t => { + const { Greeter, GreeterV2, ProxyAdmin, TransparentUpgradableProxy } = t.context; + + const owner = (await ethers.getSigners())[0]; + + const impl = await Greeter.deploy(); + await impl.waitForDeployment(); + const admin = await ProxyAdmin.deploy(owner); + await admin.waitForDeployment(); + const proxy = await TransparentUpgradableProxy.deploy( + await impl.getAddress(), + owner, + getInitializerData(Greeter.interface, ['Hello, Hardhat!']), + ); + await proxy.waitForDeployment(); + + const greeter = await upgrades.forceImport(await proxy.getAddress(), Greeter); + t.is(await greeter.greet(), 'Hello, Hardhat!'); + + const greeter2 = await upgrades.upgradeProxy(greeter, GreeterV2); + await greeter2.waitForDeployment(); + t.is(await greeter2.greet(), 'Hello, Hardhat!'); + await greeter2.resetGreeting(); + t.is(await greeter2.greet(), 'Hello World'); +}); + +test('uups happy path', async t => { + const { GreeterProxiable, GreeterV2Proxiable, ERC1967Proxy } = t.context; + + const impl = await GreeterProxiable.deploy(); + await impl.waitForDeployment(); + const proxy = await ERC1967Proxy.deploy( + await impl.getAddress(), + getInitializerData(GreeterProxiable.interface, ['Hello, Hardhat!']), + ); + await proxy.waitForDeployment(); + + const greeter = await upgrades.forceImport(await proxy.getAddress(), GreeterProxiable); + t.is(await greeter.greet(), 'Hello, Hardhat!'); + + const greeter2 = await upgrades.upgradeProxy(greeter, GreeterV2Proxiable); + await greeter2.waitForDeployment(); + t.is(await greeter2.greet(), 'Hello, Hardhat!'); + await greeter2.resetGreeting(); + t.is(await greeter2.greet(), 'Hello World'); +}); + +test('beacon proxy happy path', async t => { + const { Greeter, GreeterV2, UpgradableBeacon, BeaconProxy } = t.context; + + const owner = (await ethers.getSigners())[0]; + + const impl = await Greeter.deploy(); + await impl.waitForDeployment(); + const beacon = await UpgradableBeacon.deploy(await impl.getAddress(), owner); + await beacon.waitForDeployment(); + const proxy = await BeaconProxy.deploy( + await beacon.getAddress(), + getInitializerData(Greeter.interface, ['Hello, Hardhat!']), + ); + await proxy.waitForDeployment(); + + const greeter = await upgrades.forceImport(await proxy.getAddress(), Greeter); + t.is(await greeter.greet(), 'Hello, Hardhat!'); + + await upgrades.upgradeBeacon(beacon, GreeterV2); + const greeter2 = GreeterV2.attach(await greeter.getAddress()); + await greeter2.waitForDeployment(); + t.is(await greeter2.greet(), 'Hello, Hardhat!'); + await greeter2.resetGreeting(); + t.is(await greeter2.greet(), 'Hello World'); +}); + +test('beacon happy path', async t => { + const { Greeter, GreeterV2, UpgradableBeacon } = t.context; + + const owner = (await ethers.getSigners())[0]; + + const impl = await Greeter.deploy(); + await impl.waitForDeployment(); + const beacon = await UpgradableBeacon.deploy(await impl.getAddress(), owner); + await beacon.waitForDeployment(); + + const beaconImported = await upgrades.forceImport(await beacon.getAddress(), Greeter); + t.is(await beaconImported.implementation(), await impl.getAddress()); + + await upgrades.upgradeBeacon(beacon, GreeterV2); +}); + +test('import proxy using contract instance', async t => { + const { GreeterProxiable, GreeterV2Proxiable, ERC1967Proxy } = t.context; + + const impl = await GreeterProxiable.deploy(); + await impl.waitForDeployment(); + const proxy = await ERC1967Proxy.deploy( + await impl.getAddress(), + getInitializerData(GreeterProxiable.interface, ['Hello, Hardhat!']), + ); + await proxy.waitForDeployment(); + + const greeter = await upgrades.forceImport(proxy, GreeterProxiable); + t.is(await greeter.greet(), 'Hello, Hardhat!'); + + const greeter2 = await upgrades.upgradeProxy(greeter, GreeterV2Proxiable); + await greeter2.waitForDeployment(); + t.is(await greeter2.greet(), 'Hello, Hardhat!'); + await greeter2.resetGreeting(); + t.is(await greeter2.greet(), 'Hello World'); +}); + +test('wrong kind', async t => { + const { GreeterProxiable, GreeterV2Proxiable, ERC1967Proxy } = t.context; + + const impl = await GreeterProxiable.deploy(); + await impl.waitForDeployment(); + const proxy = await ERC1967Proxy.deploy( + await impl.getAddress(), + getInitializerData(GreeterProxiable.interface, ['Hello, Hardhat!']), + ); + await proxy.waitForDeployment(); + + // specify wrong kind + const greeter = await upgrades.forceImport(await proxy.getAddress(), GreeterProxiable, { kind: 'transparent' }); + t.is(await greeter.greet(), 'Hello, Hardhat!'); + + // an error is expected since the user force imported the wrong kind + const e = await t.throwsAsync(() => upgrades.upgradeProxy(greeter, GreeterV2Proxiable)); + t.true(e.message.startsWith(REQUESTED_UPGRADE_WRONG_KIND), e.message); +}); + +test('import custom UUPS proxy', async t => { + const { GreeterProxiable, GreeterV2Proxiable, CustomProxy } = t.context; + + const impl = await GreeterProxiable.deploy(); + await impl.waitForDeployment(); + const proxy = await CustomProxy.deploy( + await impl.getAddress(), + getInitializerData(GreeterProxiable.interface, ['Hello, Hardhat!']), + ); + await proxy.waitForDeployment(); + + const greeter = await upgrades.forceImport(await proxy.getAddress(), GreeterProxiable); + t.is(await greeter.greet(), 'Hello, Hardhat!'); + + await upgrades.upgradeProxy(greeter, GreeterV2Proxiable); +}); + +test('import custom UUPS proxy with admin', async t => { + const { GreeterProxiable, GreeterV2Proxiable, CustomProxyWithAdmin } = t.context; + + const impl = await GreeterProxiable.deploy(); + await impl.waitForDeployment(); + const proxy = await CustomProxyWithAdmin.deploy( + await impl.getAddress(), + getInitializerData(GreeterProxiable.interface, ['Hello, Hardhat!']), + ); + await proxy.waitForDeployment(); + + const greeter = await upgrades.forceImport(await proxy.getAddress(), GreeterProxiable); + t.is(await greeter.greet(), 'Hello, Hardhat!'); + + await upgrades.upgradeProxy(greeter, GreeterV2Proxiable); +}); + +test('wrong implementation', async t => { + const { Greeter, GreeterV2, ProxyAdmin, TransparentUpgradableProxy } = t.context; + + const owner = (await ethers.getSigners())[0]; + + const impl = await Greeter.deploy(); + await impl.waitForDeployment(); + const admin = await ProxyAdmin.deploy(owner); + await admin.waitForDeployment(); + const proxy = await TransparentUpgradableProxy.deploy( + await impl.getAddress(), + owner, + getInitializerData(Greeter.interface, ['Hello, Hardhat!']), + ); + await proxy.waitForDeployment(); + + const greeter = await upgrades.forceImport(await proxy.getAddress(), GreeterV2); + t.is(await greeter.greet(), 'Hello, Hardhat!'); + + // since this is the wrong impl, expect it to have an error if using a non-existent function + const e = await t.throwsAsync(() => greeter.resetGreeting()); + t.true(e.message.includes('Transaction reverted'), e.message); +}); + +test('multiple identical implementations', async t => { + const { GreeterProxiable, GreeterV2Proxiable, ERC1967Proxy } = t.context; + + const impl = await GreeterProxiable.deploy(); + await impl.waitForDeployment(); + const proxy = await ERC1967Proxy.deploy( + await impl.getAddress(), + getInitializerData(GreeterProxiable.interface, ['Hello, Hardhat!']), + ); + await proxy.waitForDeployment(); + + const impl2 = await GreeterProxiable.deploy(); + await impl2.waitForDeployment(); + const proxy2 = await ERC1967Proxy.deploy( + await impl2.getAddress(), + getInitializerData(GreeterProxiable.interface, ['Hello, Hardhat 2!']), + ); + await proxy2.waitForDeployment(); + + const greeter = await upgrades.forceImport(await proxy.getAddress(), GreeterProxiable); + const greeterUpgraded = await upgrades.upgradeProxy(greeter, GreeterV2Proxiable); + t.is(await greeterUpgraded.greet(), 'Hello, Hardhat!'); + + const greeter2 = await upgrades.forceImport(await proxy2.getAddress(), GreeterProxiable); + const greeter2Upgraded = await upgrades.upgradeProxy(greeter2, GreeterV2Proxiable); + t.is(await greeter2Upgraded.greet(), 'Hello, Hardhat 2!'); +}); + +test('same implementation', async t => { + const { GreeterProxiable, ERC1967Proxy } = t.context; + + const impl = await GreeterProxiable.deploy(); + await impl.waitForDeployment(); + const proxy = await ERC1967Proxy.deploy( + await impl.getAddress(), + getInitializerData(GreeterProxiable.interface, ['Hello, Hardhat!']), + ); + await proxy.waitForDeployment(); + const proxy2 = await ERC1967Proxy.deploy( + await impl.getAddress(), + getInitializerData(GreeterProxiable.interface, ['Hello, Hardhat 2!']), + ); + await proxy2.waitForDeployment(); + + const greeter = await upgrades.forceImport(await proxy.getAddress(), GreeterProxiable); + const greeter2 = await upgrades.forceImport(await proxy2.getAddress(), GreeterProxiable); + + const implAddr1 = await upgrades.erc1967.getImplementationAddress(await greeter.getAddress()); + const implAddr2 = await upgrades.erc1967.getImplementationAddress(await greeter2.getAddress()); + t.is(implAddr2, implAddr1); +}); + +test('import transparents with different admin', async t => { + const { Greeter, GreeterV2, ProxyAdmin, TransparentUpgradableProxy } = t.context; + + const owner = (await ethers.getSigners())[0]; + + const impl = await Greeter.deploy(); + await impl.waitForDeployment(); + const admin = await ProxyAdmin.deploy(owner); + await admin.waitForDeployment(); + const proxy = await TransparentUpgradableProxy.deploy( + await impl.getAddress(), + owner, + getInitializerData(Greeter.interface, ['Hello, Hardhat!']), + ); + await proxy.waitForDeployment(); + + const owner2 = (await ethers.getSigners())[1]; + + const admin2 = await ProxyAdmin.deploy(owner2); + await admin2.waitForDeployment(); + const proxy2 = await TransparentUpgradableProxy.deploy( + await impl.getAddress(), + owner2, + getInitializerData(Greeter.interface, ['Hello, Hardhat 2!']), + ); + await proxy2.waitForDeployment(); + + const greeter = await upgrades.forceImport(await proxy.getAddress(), Greeter); + const greeter2 = await upgrades.forceImport(await proxy2.getAddress(), Greeter); + + t.not( + await upgrades.erc1967.getAdminAddress(await greeter2.getAddress()), + await upgrades.erc1967.getAdminAddress(await greeter.getAddress()), + ); + + // proxy with a different admin can be imported + const proxyAddress = await proxy.getAddress(); + await upgrades.upgradeProxy(proxyAddress, GreeterV2); +}); + +test('import transparent then upgrade with call', async t => { + const { Greeter, GreeterV2, ProxyAdmin, TransparentUpgradableProxy } = t.context; + + const owner = (await ethers.getSigners())[0]; + + const impl = await Greeter.deploy(); + await impl.waitForDeployment(); + const admin = await ProxyAdmin.deploy(owner); + await admin.waitForDeployment(); + const proxy = await TransparentUpgradableProxy.deploy( + await impl.getAddress(), + owner, + getInitializerData(Greeter.interface, ['Hello, Hardhat!']), + ); + await proxy.waitForDeployment(); + + const greeter = await upgrades.forceImport(await proxy.getAddress(), Greeter); + t.is(await greeter.greet(), 'Hello, Hardhat!'); + + await upgrades.upgradeProxy(greeter, GreeterV2, { call: 'resetGreeting' }); + t.is(await greeter.greet(), 'Hello World'); +}); diff --git a/packages/plugin-hardhat/test/uups-happy-path-50.js b/packages/plugin-hardhat/test/uups-happy-path-50.js new file mode 100644 index 000000000..389f3648c --- /dev/null +++ b/packages/plugin-hardhat/test/uups-happy-path-50.js @@ -0,0 +1,18 @@ +const test = require('ava'); + +const { ethers, upgrades } = require('hardhat'); + +test.before(async t => { + t.context.Greeter = await ethers.getContractFactory('Greeter50Proxiable'); + t.context.GreeterV2 = await ethers.getContractFactory('Greeter50V2Proxiable'); +}); + +test('happy path', async t => { + const { Greeter, GreeterV2 } = t.context; + + const greeter = await upgrades.deployProxy(Greeter, ['Hello, Hardhat!'], { kind: 'uups' }); + + const greeter2 = await upgrades.upgradeProxy(greeter, GreeterV2); + await greeter2.waitForDeployment(); + await greeter2.resetGreeting(); +}); diff --git a/packages/plugin-hardhat/test/uups-happy-path-with-call-50.js b/packages/plugin-hardhat/test/uups-happy-path-with-call-50.js new file mode 100644 index 000000000..5e9f7b79a --- /dev/null +++ b/packages/plugin-hardhat/test/uups-happy-path-with-call-50.js @@ -0,0 +1,36 @@ +const test = require('ava'); + +const { ethers, upgrades } = require('hardhat'); + +test.before(async t => { + t.context.Greeter = await ethers.getContractFactory('Greeter50Proxiable'); + t.context.GreeterV2 = await ethers.getContractFactory('Greeter50V2Proxiable'); +}); + +test('happy path - call with args', async t => { + const { Greeter, GreeterV2 } = t.context; + + const greeter = await upgrades.deployProxy(Greeter, ['Hello, Hardhat!'], { kind: 'uups' }); + + t.is(await greeter.greet(), 'Hello, Hardhat!'); + + await upgrades.upgradeProxy(greeter, GreeterV2, { + call: { fn: 'setGreeting', args: ['Called during upgrade'] }, + }); + + t.is(await greeter.greet(), 'Called during upgrade'); +}); + +test('happy path - call without args', async t => { + const { Greeter, GreeterV2 } = t.context; + + const greeter = await upgrades.deployProxy(Greeter, ['Hello, Hardhat!'], { kind: 'uups' }); + + t.is(await greeter.greet(), 'Hello, Hardhat!'); + + await upgrades.upgradeProxy(greeter, GreeterV2, { + call: 'resetGreeting', + }); + + t.is(await greeter.greet(), 'Hello World'); +}); diff --git a/yarn.lock b/yarn.lock index 8ca9950a8..189aef35d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2318,6 +2318,11 @@ dependencies: "@octokit/openapi-types" "^18.0.0" +"@openzeppelin/contracts-5.0@npm:@openzeppelin/contracts@^5.0.0-rc.0": + version "5.0.0-rc.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.0-rc.0.tgz#801345b25e27c8a27e6075b508fd9e17cc303e8d" + integrity sha512-OvYXXB1EshHue8IBakqkYCglk3Yh/NaP9HUDeoONXBmTCBD/4Oo/dU84ZJ19CG6M+lEy55I7N30xNGTT69396Q== + "@openzeppelin/contracts-upgradeable@4.8.3": version "4.8.3" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.8.3.tgz#6b076a7b751811b90fe3a172a7faeaa603e13a3f"