Skip to content

Commit

Permalink
Support Defender deployments using EOA or Safe (OpenZeppelin#967)
Browse files Browse the repository at this point in the history
  • Loading branch information
ericglau authored Feb 6, 2024
1 parent 96b62bd commit 2d7deda
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 19 deletions.
16 changes: 9 additions & 7 deletions docs/modules/ROOT/pages/api-hardhat-upgrades.adoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
= OpenZeppelin Hardhat Upgrades API

Both `deployProxy` and `upgradeProxy` functions will return instances of https://docs.ethers.io/v5/api/contract/contract[ethers.js contracts], and require https://docs.ethers.io/v5/api/contract/contract-factory[ethers.js contract factories] as arguments. For https://docs.openzeppelin.com/contracts/4.x/api/proxy#beacon[beacons], `deployBeacon` and `upgradeBeacon` will both return an upgradable beacon instance that can be used with a beacon proxy. All deploy and upgrade functions validate that the implementation contract is upgrade-safe, and will fail otherwise.
Both `deployProxy` and `upgradeProxy` functions will return instances of https://docs.ethers.io/v5/api/contract/contract[ethers.js contracts], and require https://docs.ethers.io/v5/api/contract/contract-factory[ethers.js contract factories] as arguments. For https://docs.openzeppelin.com/contracts/api/proxy#beacon[beacons], `deployBeacon` and `upgradeBeacon` will both return an upgradable beacon instance that can be used with a beacon proxy. All deploy and upgrade functions validate that the implementation contract is upgrade-safe, and will fail otherwise.

[[common-options]]
== Common Options
Expand Down Expand Up @@ -31,7 +31,9 @@ The following options are common to some functions.
* `useDefenderDeploy`: (`boolean`) Deploy contracts using OpenZeppelin Defender instead of ethers.js. See xref:defender-deploy.adoc[Using with OpenZeppelin Defender]. **Note**: OpenZeppelin Defender deployments is in beta and functionality related to it is subject to change.
* `verifySourceCode`: (`boolean`) When using OpenZeppelin Defender deployments, whether to verify source code on block explorers. Defaults to `true`.
* `relayerId`: (`string`) When using OpenZeppelin Defender deployments, the ID of the relayer to use for the deployment. Defaults to the relayer configured for your deployment environment on Defender.
* `salt`: (`string`) When using OpenZeppelin Defender deployments, deployments are performed using the CREATE2 opcode. Use this option to provide the salt for the deployment. Defaults to a random salt.
* `salt`: (`string`) When using OpenZeppelin Defender deployments, if this is not set, deployments will be performed using the CREATE opcode. If this is set, deployments will be performed using the CREATE2 opcode with the provided salt. Note that deployments using a Safe are done using CREATE2 and require a salt. **Warning:** CREATE2 affects `msg.sender` behavior. See https://docs.openzeppelin.com/defender/v2/tutorial/deploy#deploy-caveat[Caveats] for more information.



Note that the options `unsafeAllow` can also be specified in a more granular way directly in the source code if using Solidity >=0.8.2. See xref:faq.adoc#how-can-i-disable-checks[How can I disable some of the checks?]

Expand Down Expand Up @@ -136,7 +138,7 @@ async function deployBeacon(
): Promise<ethers.Contract>
----

Creates an https://docs.openzeppelin.com/contracts/4.x/api/proxy#UpgradeableBeacon[upgradable beacon] given an ethers contract factory to use as implementation, and returns the beacon contract instance.
Creates an https://docs.openzeppelin.com/contracts/api/proxy#UpgradeableBeacon[upgradable beacon] given an ethers contract factory to use as implementation, and returns the beacon contract instance.

*Parameters:*

Expand Down Expand Up @@ -173,7 +175,7 @@ async function upgradeBeacon(
): Promise<ethers.Contract>
----

Upgrades an https://docs.openzeppelin.com/contracts/4.x/api/proxy#UpgradeableBeacon[upgradable beacon] at a specified address to a new implementation contract, and returns the beacon contract instance.
Upgrades an https://docs.openzeppelin.com/contracts/api/proxy#UpgradeableBeacon[upgradable beacon] at a specified address to a new implementation contract, and returns the beacon contract instance.

*Parameters:*

Expand Down Expand Up @@ -207,7 +209,7 @@ async function deployBeaconProxy(
): Promise<ethers.Contract>
----

Creates a https://docs.openzeppelin.com/contracts/4.x/api/proxy#BeaconProxy[Beacon proxy] given an existing beacon contract address and an ethers contract factory corresponding to the beacon's current implementation contract, and returns a contract instance with the beacon proxy address and the implementation interface. If `args` is set, will call an initializer function `initialize` with the supplied args during proxy deployment.
Creates a https://docs.openzeppelin.com/contracts/api/proxy#BeaconProxy[Beacon proxy] given an existing beacon contract address and an ethers contract factory corresponding to the beacon's current implementation contract, and returns a contract instance with the beacon proxy address and the implementation interface. If `args` is set, will call an initializer function `initialize` with the supplied args during proxy deployment.

*Parameters:*

Expand Down Expand Up @@ -590,8 +592,8 @@ Similar to `prepareUpgrade`. This method validates and deploys the new implement
* `opts` - an object with options:
** `title`: title of the upgrade proposal as seen in Defender Admin, defaults to `Upgrade to 0x12345678` (using the first 8 digits of the new implementation address)
** `description`: description of the upgrade proposal as seen in Defender Admin, defaults to the full implementation address.
** `multisig`: address of the multisignature wallet contract with the rights to execute the upgrade. This is autodetected in https://docs.openzeppelin.com/contracts/4.x/api/proxy#TransparentUpgradeableProxy[Transparent proxies], but required for https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable[UUPS proxies] (read more https://docs.openzeppelin.com/contracts/4.x/api/proxy#transparent-vs-uups[here]). Both Gnosis Safe and Gnosis MultisigWallet multisigs are supported.
** `proxyAdmin`: address of the https://docs.openzeppelin.com/contracts/4.x/api/proxy#ProxyAdmin[`ProxyAdmin`] contract that manages the proxy, if exists. This is autodetected in https://docs.openzeppelin.com/contracts/4.x/api/proxy#TransparentUpgradeableProxy[Transparent proxies], but required for https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable[UUPS proxies] (read more https://docs.openzeppelin.com/contracts/4.x/api/proxy#transparent-vs-uups[here]), though UUPS proxies typically do not require the usage of a ProxyAdmin.
** `multisig`: address of the multisignature wallet contract with the rights to execute the upgrade. This is autodetected in https://docs.openzeppelin.com/contracts/api/proxy#TransparentUpgradeableProxy[Transparent proxies], but required for https://docs.openzeppelin.com/contracts/api/proxy#UUPSUpgradeable[UUPS proxies] (read more https://docs.openzeppelin.com/contracts/api/proxy#transparent-vs-uups[here]). Both Gnosis Safe and Gnosis MultisigWallet multisigs are supported.
** `proxyAdmin`: address of the https://docs.openzeppelin.com/contracts/api/proxy#ProxyAdmin[`ProxyAdmin`] contract that manages the proxy, if exists. This is autodetected in https://docs.openzeppelin.com/contracts/api/proxy#TransparentUpgradeableProxy[Transparent proxies], but required for https://docs.openzeppelin.com/contracts/api/proxy#UUPSUpgradeable[UUPS proxies] (read more https://docs.openzeppelin.com/contracts/api/proxy#transparent-vs-uups[here]), though UUPS proxies typically do not require the usage of a ProxyAdmin.
** additional options as described in <<common-options>>.

*Returns:*
Expand Down
27 changes: 15 additions & 12 deletions packages/plugin-hardhat/src/defender/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,23 @@ export async function defenderDeploy(
}
}

if (deploymentResponse.address === undefined) {
throw new UpgradesError(
`Deployment response with id ${deploymentResponse.deploymentId} does not include a contract address`,
() =>
'The Hardhat Upgrades plugin is not currently compatible with this type of deployment. Use a relayer for your default deploy approval process in Defender.',
// For EOA or Safe deployments, address and/or txHash are not known until the deployment is completed.
// In this case, prompt the user to submit the deployment in Defender, and wait for it to be completed.
if (deploymentResponse.address === undefined || deploymentResponse.txHash === undefined) {
console.log(
`ACTION REQUIRED: Go to https://defender.openzeppelin.com/v2/#/deploy to submit the pending deployment.`,
);
}

if (deploymentResponse.txHash === undefined) {
throw new UpgradesError(
`Deployment response with id ${deploymentResponse.deploymentId} does not include a transaction hash`,
() =>
'The Hardhat Upgrades plugin is not currently compatible with this type of deployment. Use a relayer for your default deploy approval process in Defender.',
console.log(`The process will continue automatically when the pending deployment is completed.`);
console.log(
`Waiting for pending deployment of contract ${contractInfo.contractName} with deployment id ${deploymentResponse.deploymentId}...`,
);

const pollInterval = opts.pollingInterval ?? 5e3;
while (deploymentResponse.address === undefined || deploymentResponse.txHash === undefined) {
debug(`Waiting for deployment id ${deploymentResponse.deploymentId} to return address and txHash...`);
await new Promise(resolve => setTimeout(resolve, pollInterval));
deploymentResponse = await client.getDeployedContract(deploymentResponse.deploymentId);
}
}

const txResponse = (await hre.ethers.provider.getTransaction(deploymentResponse.txHash)) ?? undefined;
Expand Down
78 changes: 78 additions & 0 deletions packages/plugin-hardhat/test/defender-deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -441,3 +441,81 @@ test('calls defender deploy with txOverrides.maxFeePerGas and txOverrides.maxPri

assertResult(t, result);
});

test('waits until address is available', async t => {
const getDeployedContractStub = sinon.stub();
getDeployedContractStub.onFirstCall().returns({
deploymentId: DEPLOYMENT_ID,
});
getDeployedContractStub.onSecondCall().returns({
deploymentId: DEPLOYMENT_ID,
txHash: TX_HASH,
});
getDeployedContractStub.onThirdCall().returns({
deploymentId: DEPLOYMENT_ID,
txHash: TX_HASH,
address: ADDRESS,
});

await testGetDeployedContractPolling(t, getDeployedContractStub, 3);
});

test('waits until txHash is available', async t => {
const getDeployedContractStub = sinon.stub();
getDeployedContractStub.onFirstCall().returns({
deploymentId: DEPLOYMENT_ID,
});
getDeployedContractStub.onSecondCall().returns({
deploymentId: DEPLOYMENT_ID,
address: ADDRESS,
});
getDeployedContractStub.onThirdCall().returns({
deploymentId: DEPLOYMENT_ID,
txHash: TX_HASH,
address: ADDRESS,
});

await testGetDeployedContractPolling(t, getDeployedContractStub, 3);
});

async function testGetDeployedContractPolling(t, getDeployedContractStub, expectedCallCount) {
const { fakeHre, fakeChainId } = t.context;

const contractName = 'Greeter';

const defenderClientWaits = {
deployContract: () => {
return {
deploymentId: DEPLOYMENT_ID,
};
},
getDeployedContract: getDeployedContractStub,
};
const deployContractSpy = sinon.spy(defenderClientWaits, 'deployContract');

const deployPending = proxyquire('../dist/defender/deploy', {
'./utils': {
...require('../dist/defender/utils'),
getNetwork: () => fakeChainId,
getDeployClient: () => defenderClientWaits,
},
'../utils/etherscan-api': {
getEtherscanAPIConfig: () => {
return { key: ETHERSCAN_API_KEY };
},
},
});

const factory = await ethers.getContractFactory(contractName);
const result = await deployPending.defenderDeploy(fakeHre, factory, { pollingInterval: 1 }); // poll in 1 ms

t.is(deployContractSpy.callCount, 1);
t.is(getDeployedContractStub.callCount, expectedCallCount);

t.deepEqual(result, {
address: ADDRESS,
txHash: TX_HASH,
deployTransaction: TX_RESPONSE,
remoteDeploymentId: DEPLOYMENT_ID,
});
}

0 comments on commit 2d7deda

Please sign in to comment.