Skip to content

Commit

Permalink
Options to change deploy and factory functions
Browse files Browse the repository at this point in the history
Adds two new options that allow override of the deploy and factory
functions.

Kind is still used to identify the kind of proxy used, but this
customization can manage variations of the proxy implementation and its
constructor parameters.

As a test case, and to show a concrete application of this change, I
implemented a AccessManagedProxy. This proxy works like a UUPS (ERC1967)
proxy, but before delegating the calls, checks with the access manager
if the call is allowed.
  • Loading branch information
gnarvaja committed Nov 28, 2024
1 parent 9d3112a commit 09113a6
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 6 deletions.
26 changes: 26 additions & 0 deletions packages/plugin-hardhat/contracts/AccessManagedProxy.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
4 changes: 2 additions & 2 deletions packages/plugin-hardhat/src/deploy-beacon-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProxyDeployment> & 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);
Expand Down
12 changes: 8 additions & 4 deletions packages/plugin-hardhat/src/deploy-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}
Expand Down
19 changes: 19 additions & 0 deletions packages/plugin-hardhat/src/utils/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
/**
Expand All @@ -32,6 +48,8 @@ export function withDefaults(opts: UpgradeOptions = {}): Required<UpgradeOptions
constructorArgs: opts.constructorArgs ?? [],
timeout: opts.timeout ?? 60e3,
pollingInterval: opts.pollingInterval ?? 5e3,
proxyFactory: null,
deployFunction: null,
useDeployedImplementation: opts.useDeployedImplementation ?? false,
redeployImplementation: opts.redeployImplementation ?? 'onchange',
txOverrides: opts.txOverrides ?? {},
Expand Down Expand Up @@ -91,6 +109,7 @@ export type InitialOwner = {

export type DeployBeaconProxyOptions = EthersDeployOptions &
DeployOpts &
DeployFactoryOpts &
ProxyKindOption &
Initializer &
DefenderDeployOptions;
Expand Down
57 changes: 57 additions & 0 deletions packages/plugin-hardhat/test/uups-accessmanaged-proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const test = require('ava');

const { ethers, upgrades } = require('hardhat');
const { deploy } = require('../dist/utils/deploy');

test.before(async t => {
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');
});

0 comments on commit 09113a6

Please sign in to comment.