diff --git a/packages/plugin-hardhat/contracts/AccessManagedProxy.sol b/packages/plugin-hardhat/contracts/AccessManagedProxy.sol new file mode 100644 index 000000000..423a01328 --- /dev/null +++ b/packages/plugin-hardhat/contracts/AccessManagedProxy.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {AccessManager} from "@openzeppelin/contracts/access/manager/AccessManager.sol"; // Artifact for test +import {IAccessManager} from "@openzeppelin/contracts/access/manager/IAccessManager.sol"; +import {IAccessManaged} from "@openzeppelin/contracts/access/manager/IAccessManaged.sol"; + +contract AccessManagedProxy is ERC1967Proxy { + IAccessManager public immutable ACCESSMANAGER; + + constructor(address implementation, bytes memory _data, IAccessManager manager) payable ERC1967Proxy(implementation, _data) { + ACCESSMANAGER = manager; + } + + /** + * @dev Checks with the ACCESSMANAGER if the method can be called by msg.sender and then . + * + * This function does not return to its internal call site, it will return directly to the external caller. + */ + function _delegate(address implementation) internal virtual override { + (bool immediate, ) = ACCESSMANAGER.canCall(msg.sender, address(this), bytes4(msg.data[0:4])); + if (!immediate) revert IAccessManaged.AccessManagedUnauthorized(msg.sender); + super._delegate(implementation); + } +} diff --git a/packages/plugin-hardhat/src/deploy-beacon-proxy.ts b/packages/plugin-hardhat/src/deploy-beacon-proxy.ts index 68514532f..5e5a07383 100644 --- a/packages/plugin-hardhat/src/deploy-beacon-proxy.ts +++ b/packages/plugin-hardhat/src/deploy-beacon-proxy.ts @@ -80,10 +80,10 @@ export function makeDeployBeaconProxy( ]); } - const BeaconProxyFactory = await getBeaconProxyFactory(hre, getSigner(attachTo.runner)); + const BeaconProxyFactory = await (opts.proxyFactory || getBeaconProxyFactory)(hre, getSigner(attachTo.runner)); const proxyDeployment: Required & DeployTransaction & RemoteDeploymentId = Object.assign( { kind: opts.kind }, - await deploy(hre, opts, BeaconProxyFactory, beaconAddress, data), + await (opts.deployFunction || deploy)(hre, opts, BeaconProxyFactory, beaconAddress, data), ); await manifest.addProxy(proxyDeployment); diff --git a/packages/plugin-hardhat/src/deploy-proxy.ts b/packages/plugin-hardhat/src/deploy-proxy.ts index 1fdf68b0b..295a1cdf2 100644 --- a/packages/plugin-hardhat/src/deploy-proxy.ts +++ b/packages/plugin-hardhat/src/deploy-proxy.ts @@ -51,6 +51,7 @@ export function makeDeployProxy(hre: HardhatRuntimeEnvironment, defenderModule: const contractInterface = ImplFactory.interface; const data = getInitializerData(contractInterface, args, opts.initializer); + const deployFn = opts.deployFunction || deploy; if (await manifest.getAdmin()) { if (kind === 'uups') { @@ -79,8 +80,8 @@ export function makeDeployProxy(hre: HardhatRuntimeEnvironment, defenderModule: throw new InitialOwnerUnsupportedKindError(kind); } - const ProxyFactory = await getProxyFactory(hre, signer); - proxyDeployment = Object.assign({ kind }, await deploy(hre, opts, ProxyFactory, impl, data)); + const ProxyFactory = await (opts.proxyFactory || getProxyFactory)(hre, signer); + proxyDeployment = Object.assign({ kind }, await deployFn(hre, opts, ProxyFactory, impl, data)); break; } @@ -95,10 +96,13 @@ export function makeDeployProxy(hre: HardhatRuntimeEnvironment, defenderModule: ); } - const TransparentUpgradeableProxyFactory = await getTransparentUpgradeableProxyFactory(hre, signer); + const TransparentUpgradeableProxyFactory = await (opts.proxyFactory || getTransparentUpgradeableProxyFactory)( + hre, + signer, + ); proxyDeployment = Object.assign( { kind }, - await deploy(hre, opts, TransparentUpgradeableProxyFactory, impl, initialOwner, data), + await deployFn(hre, opts, TransparentUpgradeableProxyFactory, impl, initialOwner, data), ); break; } diff --git a/packages/plugin-hardhat/src/utils/options.ts b/packages/plugin-hardhat/src/utils/options.ts index cd2ba413c..c4a913747 100644 --- a/packages/plugin-hardhat/src/utils/options.ts +++ b/packages/plugin-hardhat/src/utils/options.ts @@ -8,11 +8,27 @@ import { } from '@openzeppelin/upgrades-core'; import { Overrides } from 'ethers'; +/** + * Options for customizing the factory or deploy functions + */ +export type DeployFactoryOpts = { + /** + * Allows to customize the proxyFactory used instead of the ones defined in utils/factories.ts + */ + proxyFactory?: null | Function; + + /** + * Allows to customize the deploy function used instead of utils/deploy.ts:deploy + */ + deployFunction?: null | Function; +}; + /** * Options for functions that can deploy an implementation contract. */ export type StandaloneOptions = StandaloneValidationOptions & DeployOpts & + DeployFactoryOpts & EthersDeployOptions & { constructorArgs?: unknown[]; /** @@ -32,6 +48,8 @@ export function withDefaults(opts: UpgradeOptions = {}): Required { + t.context.Greeter = await ethers.getContractFactory('GreeterProxiable'); + t.context.GreeterV2 = await ethers.getContractFactory('GreeterV2Proxiable'); + t.context.GreeterV3 = await ethers.getContractFactory('GreeterV3Proxiable'); + t.context.AccessManagedProxy = await ethers.getContractFactory('AccessManagedProxy'); + const AccessManager = await ethers.getContractFactory('AccessManager'); + const [admin, anon] = await ethers.getSigners(); + t.context.anon = anon; + t.context.admin = admin; + t.context.acMgr = await AccessManager.deploy(admin); +}); + +async function getAccessManagedProxy(hre, signer) { + return hre.ethers.getContractFactory('AccessManagedProxy', signer); +} + +async function deployWithExtraProxyArgs(hre, opts, factory, ...args) { + const allArgs = [...args, ...(opts.proxyExtraConstructorArgs || [])]; + return deploy(hre, opts, factory, ...allArgs); +} + +test('accessmanaged proxy / customization of deploy and factory functions', async t => { + const { Greeter, GreeterV2, GreeterV3, acMgr, anon, admin } = t.context; + + const greeter = await upgrades.deployProxy(Greeter, ['Hello, Hardhat!'], { + kind: 'uups', + proxyExtraConstructorArgs: [await acMgr.getAddress()], + deployFunction: deployWithExtraProxyArgs, + proxyFactory: getAccessManagedProxy, + }); + + // By default it calls from admin address, so, it works fine + let greet = await greeter.connect(admin).greet(); + t.is(greet, 'Hello, Hardhat!'); + // But fails when called from other user + let e = await t.throwsAsync(() => greeter.connect(anon).greet()); + t.true(e.message.includes('AccessManagedUnauthorized'), e.message); + + // Upgrades work well, because the call executed from the default signer that is the admin + const greeter2 = await upgrades.upgradeProxy(greeter, GreeterV2); + await greeter2.waitForDeployment(); + await greeter2.resetGreeting(); + + // Upgrades don't break the access control + e = await t.throwsAsync(() => greeter2.connect(anon).resetGreeting()); + t.true(e.message.includes('AccessManagedUnauthorized'), e.message); + + const greeter3ImplAddr = await upgrades.prepareUpgrade(await greeter.getAddress(), GreeterV3); + const greeter3 = GreeterV3.attach(greeter3ImplAddr); + const version3 = await greeter3.version(); + t.is(version3, 'V3'); +});