From 6248d96f3df46f32782c9c4cc56be9d96533839e Mon Sep 17 00:00:00 2001 From: Naman Attri <50364269+namanB8@users.noreply.github.com> Date: Sun, 21 Jul 2024 18:31:41 +0530 Subject: [PATCH 01/13] installs safe global dependencies --- packages/plugin-hardhat/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/plugin-hardhat/package.json b/packages/plugin-hardhat/package.json index 7e2f21e4f..82d8407a9 100644 --- a/packages/plugin-hardhat/package.json +++ b/packages/plugin-hardhat/package.json @@ -39,6 +39,9 @@ "@openzeppelin/defender-sdk-deploy-client": "^1.10.0", "@openzeppelin/defender-sdk-network-client": "^1.10.0", "@openzeppelin/upgrades-core": "^1.32.0", + "@safe-global/api-kit": "^2.4.3", + "@safe-global/protocol-kit": "^4.0.3", + "@safe-global/safe-core-sdk-types": "^5.0.3", "chalk": "^4.1.0", "debug": "^4.1.1", "ethereumjs-util": "^7.1.5", From 11b02da672f986f22d52c5322434cdbcb03821c9 Mon Sep 17 00:00:00 2001 From: Naman Attri <50364269+namanB8@users.noreply.github.com> Date: Sun, 21 Jul 2024 18:39:39 +0530 Subject: [PATCH 02/13] safe global deploy --- packages/core/contracts/CreateCall.sol | 48 +++++ .../plugin-hardhat/src/safeglobal/deploy.ts | 179 ++++++++++++++++++ packages/plugin-hardhat/src/utils/deploy.ts | 7 +- packages/plugin-hardhat/src/utils/options.ts | 49 ++++- yarn.lock | 56 +++++- 5 files changed, 327 insertions(+), 12 deletions(-) create mode 100644 packages/core/contracts/CreateCall.sol create mode 100644 packages/plugin-hardhat/src/safeglobal/deploy.ts diff --git a/packages/core/contracts/CreateCall.sol b/packages/core/contracts/CreateCall.sol new file mode 100644 index 000000000..1a7988d84 --- /dev/null +++ b/packages/core/contracts/CreateCall.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: LGPL-3.0-only + +pragma solidity >=0.7.0 <0.9.0; + +/** + * @title Create Call - Allows to use the different create opcodes to deploy a contract. + * @author Richard Meissner - @rmeissner + * @notice This contract provides functions for deploying a new contract using the create and create2 opcodes. + */ +contract CreateCall { + /// @notice Emitted when a new contract is created + event ContractCreation(address indexed newContract); + + /** + * @notice Deploys a new contract using the create2 opcode. + * @param value The value in wei to be sent with the contract creation. + * @param deploymentData The initialisation code of the contract to be created. + * @param salt The salt value to use for the contract creation. + * @return newContract The address of the newly created contract. + */ + function performCreate2(uint256 value, bytes memory deploymentData, bytes32 salt) public returns (address newContract) { + /* solhint-disable no-inline-assembly */ + /// @solidity memory-safe-assembly + assembly { + newContract := create2(value, add(0x20, deploymentData), mload(deploymentData), salt) + } + /* solhint-enable no-inline-assembly */ + require(newContract != address(0), "Could not deploy contract"); + emit ContractCreation(newContract); + } + + /** + * @notice Deploys a new contract using the create opcode. + * @param value The value in wei to be sent with the contract creation. + * @param deploymentData The initialisation code of the contract to be created. + * @return newContract The address of the newly created contract. + */ + function performCreate(uint256 value, bytes memory deploymentData) public returns (address newContract) { + /* solhint-disable no-inline-assembly */ + /// @solidity memory-safe-assembly + assembly { + newContract := create(value, add(deploymentData, 0x20), mload(deploymentData)) + } + /* solhint-enable no-inline-assembly */ + require(newContract != address(0), "Could not deploy contract"); + emit ContractCreation(newContract); + } +} \ No newline at end of file diff --git a/packages/plugin-hardhat/src/safeglobal/deploy.ts b/packages/plugin-hardhat/src/safeglobal/deploy.ts new file mode 100644 index 000000000..ad774244e --- /dev/null +++ b/packages/plugin-hardhat/src/safeglobal/deploy.ts @@ -0,0 +1,179 @@ +import { type ContractFactory, Interface, id, toBigInt, TransactionResponse, TransactionReceipt } from 'ethers'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { Deployment, getChainId, RemoteDeploymentId } from '@openzeppelin/upgrades-core'; +import { MetaTransactionData, OperationType } from '@safe-global/safe-core-sdk-types'; +import SafeApiKit from '@safe-global/api-kit'; +import Safe from '@safe-global/protocol-kit'; + +import CreateCall from '@openzeppelin/upgrades-core/artifacts/contracts/CreateCall.sol/CreateCall.json'; + +import { DeployTransaction, UpgradeOptions, EthersDeployOptions, SafeGlobalDeployOptions } from '../utils'; + +export async function safeGlobalDeploy( + hre: HardhatRuntimeEnvironment, + factory: ContractFactory, + opts: UpgradeOptions & EthersDeployOptions & SafeGlobalDeployOptions, + ...args: unknown[] +): Promise & RemoteDeploymentId> { + const tx = await factory.getDeployTransaction(...args); + const chainId = hre.network.config.chainId ?? (await getChainId(hre.network.provider)); + + // create performCreate2Tx + const create2Data = await getPerformCreate2Data(tx.data, opts); + console.log('Proposing multisig deployment tx and waiting for contract to be deployed...'); + const deployTxHash = await proposeAndWaitForSafeTx(hre, create2Data, chainId, opts); + const [address, txResponse] = await getCreate2DeployedContractAddress(hre, deployTxHash); + + const deployTransaction = txResponse; + if (deployTransaction === null) { + throw new Error('Broken invariant: deploymentTransaction is null'); + } + + const txHash = deployTransaction.hash; + + return { + address, + txHash, + deployTransaction, + }; +} + +async function getPerformCreate2Data( + deployData: string, + opts: UpgradeOptions & EthersDeployOptions & SafeGlobalDeployOptions, +): Promise { + if (opts.salt === undefined || opts.salt === '' || opts.salt.trim() === '') { + throw new Error('Salt must be provided for create2 deployment'); + } + const iface = new Interface(CreateCall.abi); + const performCreate2Data = iface.encodeFunctionData('performCreate2', [0, deployData, id(opts.salt)]); + return performCreate2Data; +} + +async function proposeAndWaitForSafeTx( + hre: HardhatRuntimeEnvironment, + performCreate2Data: string, + chainId: number, + opts: UpgradeOptions & EthersDeployOptions & SafeGlobalDeployOptions, +) { + if (opts.createCallAddress === undefined || opts.createCallAddress === '') { + throw new Error('CreateCall address must be provided for create2 deployment'); + } + const performCreate2MetaTxData: MetaTransactionData = { + to: opts.createCallAddress, + data: performCreate2Data, + value: '0', + operation: OperationType.Call, + }; + + const safeTxHash = await proposeSafeTx(hre, performCreate2MetaTxData, chainId, opts); + console.log(`Safe tx hash: ${safeTxHash}`); + return await waitUntilSignedAndExecuted(safeTxHash, chainId, opts); +} + +async function proposeSafeTx( + hre: HardhatRuntimeEnvironment, + txData: MetaTransactionData, + chainId: number, + opts: UpgradeOptions & EthersDeployOptions & SafeGlobalDeployOptions, +) { + const apiKit = new SafeApiKit({ + chainId: toBigInt(chainId), + txServiceUrl: opts.txServiceUrl, + }); + + const protocolKitOwner1 = await Safe.init({ + provider: hre.network.provider, + signer: process.env.SIGNER, + safeAddress: opts.safeAddress ?? '', + contractNetworks: { + [chainId]: { + safeSingletonAddress: opts.safeSingletonAddress ?? '', + safeProxyFactoryAddress: opts.safeProxyFactoryAddress ?? '', + multiSendAddress: opts.multiSendAddress ?? '', + multiSendCallOnlyAddress: opts.multiSendCallOnlyAddress ?? '', + fallbackHandlerAddress: opts.fallbackHandlerAddress ?? '', + signMessageLibAddress: opts.signMessageLibAddress ?? '', + createCallAddress: opts.createCallAddress ?? '', + simulateTxAccessorAddress: opts.simulateTxAccessorAddress ?? '', + }, + }, + }); + + const safeAddress = await protocolKitOwner1.getAddress(); + + // Sign and send the transaction + // Create a Safe transaction with the provided parameters + const safeTransaction = await protocolKitOwner1.createTransaction({ transactions: [txData] }); + + const safeTxHash = await protocolKitOwner1.getTransactionHash(safeTransaction); + + const senderSignature = await protocolKitOwner1.signHash(safeTxHash); + + await apiKit.proposeTransaction({ + safeAddress, + safeTransactionData: safeTransaction.data, + safeTxHash, + senderAddress: '0x25843121E84f6E52a140885986FD890d302c34EE', + senderSignature: senderSignature.data, + }); + + return safeTxHash; +} + +async function waitUntilSignedAndExecuted( + safeTxHash: string, + chainId: number, + opts: UpgradeOptions & EthersDeployOptions & SafeGlobalDeployOptions, +) { + const apiKit = new SafeApiKit({ + chainId: toBigInt(chainId), + txServiceUrl: opts.txServiceUrl, + }); + + const safeTx = await apiKit.getTransaction(safeTxHash); + if (safeTx.isExecuted) { + return safeTx.transactionHash; + } + + return new Promise(resolve => { + const interval = setInterval(async () => { + const safeTx = await apiKit.getTransaction(safeTxHash); + if (safeTx.isExecuted) { + clearInterval(interval); + resolve(safeTx.transactionHash); + } + }, 1000); + }); +} + +async function getCreate2DeployedContractAddress( + hre: HardhatRuntimeEnvironment, + txHash: string, +): Promise<[string, TransactionResponse | null, TransactionReceipt | null]> { + const iface = new Interface(CreateCall.abi); + const provider = hre.ethers.provider; + const tx = await provider.getTransaction(txHash); + const receipt = await provider.getTransactionReceipt(txHash); + + if (receipt === null) { + console.log('Transaction not found or not yet mined.'); + return ['', tx, receipt]; + } + + // Parse logs + for (const log of receipt.logs) { + try { + const parsedLog = iface.parseLog(log); + if (parsedLog?.name === 'ContractCreation') { + console.log(`Event: ${parsedLog?.name}`); + console.log(`New Contract: ${parsedLog?.args.newContract}`); + return [parsedLog?.args.newContract, tx, receipt]; + } + } catch (error) { + console.error('Error parsing log:', error); + } + } + return ['', tx, receipt]; +} diff --git a/packages/plugin-hardhat/src/utils/deploy.ts b/packages/plugin-hardhat/src/utils/deploy.ts index fab957310..a80526878 100644 --- a/packages/plugin-hardhat/src/utils/deploy.ts +++ b/packages/plugin-hardhat/src/utils/deploy.ts @@ -2,7 +2,8 @@ import type { Deployment, RemoteDeploymentId } from '@openzeppelin/upgrades-core import type { ethers, ContractFactory, ContractMethodArgs } from 'ethers'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { defenderDeploy } from '../defender/deploy'; -import { EthersDeployOptions, DefenderDeployOptions, UpgradeOptions } from './options'; +import { EthersDeployOptions, DefenderDeployOptions, UpgradeOptions, SafeGlobalDeployOptions } from './options'; +import { safeGlobalDeploy } from '../safeglobal/deploy'; export interface DeployTransaction { deployTransaction?: ethers.TransactionResponse; @@ -10,13 +11,15 @@ export interface DeployTransaction { export async function deploy( hre: HardhatRuntimeEnvironment, - opts: UpgradeOptions & EthersDeployOptions & DefenderDeployOptions, + opts: UpgradeOptions & EthersDeployOptions & DefenderDeployOptions & SafeGlobalDeployOptions, factory: ContractFactory, ...args: unknown[] ): Promise & DeployTransaction & RemoteDeploymentId> { // defender always includes RemoteDeploymentId, while ethers always includes DeployTransaction if (opts?.useDefenderDeploy) { return await defenderDeploy(hre, factory, opts, ...args); + } else if (opts?.useSafeGlobalDeploy) { + return await safeGlobalDeploy(hre, factory, opts, ...args); } else { if (opts.txOverrides !== undefined) { args.push(opts.txOverrides); diff --git a/packages/plugin-hardhat/src/utils/options.ts b/packages/plugin-hardhat/src/utils/options.ts index 318416fdf..434b90c42 100644 --- a/packages/plugin-hardhat/src/utils/options.ts +++ b/packages/plugin-hardhat/src/utils/options.ts @@ -69,6 +69,30 @@ export type DefenderDeployOptions = DefenderDeploy & { skipLicenseType?: boolean; }; +/** + * Option to enable or disable SafeGlobal deployments. + */ +export type SafeGlobalDeploy = { + useSafeGlobalDeploy?: boolean; +}; + +/** + * Options for functions that support Defender deployments. + */ +export type SafeGlobalDeployOptions = SafeGlobalDeploy & { + txServiceUrl?: string; + salt?: string; + safeAddress?: string; + safeSingletonAddress?: string; + safeProxyFactoryAddress?: string; + multiSendAddress?: string; + multiSendCallOnlyAddress?: string; + fallbackHandlerAddress?: string; + signMessageLibAddress?: string; + createCallAddress?: string; + simulateTxAccessorAddress?: string; +}; + /** * Options for functions that support deployments through ethers.js. */ @@ -87,20 +111,29 @@ export type DeployBeaconProxyOptions = EthersDeployOptions & DeployOpts & ProxyKindOption & Initializer & - DefenderDeployOptions; -export type DeployBeaconOptions = StandaloneOptions & InitialOwner & DefenderDeploy; -export type DeployImplementationOptions = StandaloneOptions & GetTxResponse & DefenderDeployOptions; + DefenderDeployOptions & + SafeGlobalDeployOptions; +export type DeployBeaconOptions = StandaloneOptions & InitialOwner & DefenderDeploy & SafeGlobalDeployOptions; +export type DeployImplementationOptions = StandaloneOptions & + GetTxResponse & + DefenderDeployOptions & + SafeGlobalDeployOptions; export type DeployContractOptions = Omit & // ethers deployment not supported for deployContract GetTxResponse & DefenderDeployOptions & { unsafeAllowDeployContract?: boolean; - }; -export type DeployProxyOptions = StandaloneOptions & Initializer & InitialOwner & DefenderDeployOptions; + } & SafeGlobalDeployOptions; +export type DeployProxyOptions = StandaloneOptions & + Initializer & + InitialOwner & + DefenderDeployOptions & + SafeGlobalDeployOptions; export type ForceImportOptions = ProxyKindOption; -export type PrepareUpgradeOptions = UpgradeOptions & GetTxResponse & DefenderDeployOptions; -export type UpgradeBeaconOptions = UpgradeOptions & DefenderDeploy; +export type PrepareUpgradeOptions = UpgradeOptions & GetTxResponse & DefenderDeployOptions & SafeGlobalDeployOptions; +export type UpgradeBeaconOptions = UpgradeOptions & DefenderDeploy & SafeGlobalDeployOptions; export type UpgradeProxyOptions = UpgradeOptions & { call?: { fn: string; args?: unknown[] } | string; -} & DefenderDeploy; +} & DefenderDeploy & + SafeGlobalDeployOptions; export type ValidateImplementationOptions = StandaloneValidationOptions; export type ValidateUpgradeOptions = ValidationOptions; diff --git a/yarn.lock b/yarn.lock index 5699c59c6..8581985b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -589,6 +589,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== +"@noble/hashes@^1.3.3": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + "@noble/secp256k1@1.7.1", "@noble/secp256k1@~1.7.0": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -847,6 +852,43 @@ estree-walker "^2.0.1" picomatch "^2.2.2" +"@safe-global/api-kit@^2.4.3": + version "2.4.3" + resolved "https://registry.yarnpkg.com/@safe-global/api-kit/-/api-kit-2.4.3.tgz#5bde12efa15c98e96037880718d86a00d155f7d1" + integrity sha512-kIbRMYWQiVUqIpu65GYTiHQgYDne9CZJ7JREmtrLQh11n3K7+LxNmHT6HW1VfVj/5Es8fAQlwrBukbTy0AQwLw== + dependencies: + "@safe-global/protocol-kit" "^4.0.3" + "@safe-global/safe-core-sdk-types" "^5.0.3" + ethers "^6.13.1" + node-fetch "^2.7.0" + +"@safe-global/protocol-kit@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@safe-global/protocol-kit/-/protocol-kit-4.0.3.tgz#d8195bd4d7839dd816bac4211bd08813e1ad3780" + integrity sha512-r/HguW5jRqC7dXa+2uy2k233b3XKASrnjOuF7ZFtybXqxRMCuAjptM1Veqmhj+/olLRuh7toupwEtRTYepBgMQ== + dependencies: + "@noble/hashes" "^1.3.3" + "@safe-global/safe-core-sdk-types" "^5.0.3" + "@safe-global/safe-deployments" "^1.37.1" + abitype "^1.0.2" + ethereumjs-util "^7.1.5" + ethers "^6.13.1" + semver "^7.6.2" + +"@safe-global/safe-core-sdk-types@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@safe-global/safe-core-sdk-types/-/safe-core-sdk-types-5.0.3.tgz#0093898a0585efcae01b4da9cdd5998bc1f4a251" + integrity sha512-SNoIq/bYeUvxtB9bn+9FVMcCW3SCOJaK6crRN7DXY+N2xaLtTMAaGeUCPuOGsHxfAJVkO+CdiwWNFoqt9GN0Zg== + dependencies: + abitype "^1.0.2" + +"@safe-global/safe-deployments@^1.37.1": + version "1.37.2" + resolved "https://registry.yarnpkg.com/@safe-global/safe-deployments/-/safe-deployments-1.37.2.tgz#d63947bf631d63f5991f3732fad08b17b7264505" + integrity sha512-kWRim5vY9W/yNKUUehUQDhCHz7NWzXjhkpMDvvnrrkEn9U461zwCcmJmPVnPrjnaY4dPpJsGZSzUuU4+uxH+vg== + dependencies: + semver "^7.6.2" + "@scure/base@~1.1.0": version "1.1.7" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.7.tgz#fe973311a5c6267846aa131bc72e96c5d40d2b30" @@ -1226,6 +1268,11 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abitype@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.5.tgz#29d0daa3eea867ca90f7e4123144c1d1270774b6" + integrity sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw== + acorn-import-attributes@^1.9.2: version "1.9.5" resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" @@ -2629,7 +2676,7 @@ ethereumjs-util@^7.0.3, ethereumjs-util@^7.1.5: ethereum-cryptography "^0.1.3" rlp "^2.2.4" -ethers@^6.8.1: +ethers@^6.13.1, ethers@^6.8.1: version "6.13.1" resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.13.1.tgz#2b9f9c7455cde9d38b30fe6589972eb083652961" integrity sha512-hdJ2HOxg/xx97Lm9HdCWk949BfYqYWpyw4//78SiwOLgASyfrNszfMUNB2joKjvGUdwhHfaiMMFFwacVVoLR9A== @@ -4194,7 +4241,7 @@ node-addon-api@^2.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== -node-fetch@^2.6.1, node-fetch@^2.6.7: +node-fetch@^2.6.1, node-fetch@^2.6.7, node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -4921,6 +4968,11 @@ semver@^7.3.2, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== +semver@^7.6.2: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + serialize-error@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" From d70f301b8d7c9d46bf11576f3ea569e9ffa59339 Mon Sep 17 00:00:00 2001 From: Naman Attri <50364269+namanB8@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:24:37 +0530 Subject: [PATCH 03/13] adds upgradeAndCall wrapper via safe global --- .../plugin-hardhat/src/safeglobal/deploy.ts | 8 +- .../plugin-hardhat/src/safeglobal/upgrade.ts | 73 +++++++++++++++++++ packages/plugin-hardhat/src/upgrade-proxy.ts | 5 ++ 3 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 packages/plugin-hardhat/src/safeglobal/upgrade.ts diff --git a/packages/plugin-hardhat/src/safeglobal/deploy.ts b/packages/plugin-hardhat/src/safeglobal/deploy.ts index ad774244e..acb9d886d 100644 --- a/packages/plugin-hardhat/src/safeglobal/deploy.ts +++ b/packages/plugin-hardhat/src/safeglobal/deploy.ts @@ -67,17 +67,17 @@ async function proposeAndWaitForSafeTx( operation: OperationType.Call, }; - const safeTxHash = await proposeSafeTx(hre, performCreate2MetaTxData, chainId, opts); + const safeTxHash = await proposeSafeTx(hre, performCreate2MetaTxData, opts); console.log(`Safe tx hash: ${safeTxHash}`); return await waitUntilSignedAndExecuted(safeTxHash, chainId, opts); } -async function proposeSafeTx( +export async function proposeSafeTx( hre: HardhatRuntimeEnvironment, txData: MetaTransactionData, - chainId: number, opts: UpgradeOptions & EthersDeployOptions & SafeGlobalDeployOptions, ) { + const chainId = hre.network.config.chainId ?? (await getChainId(hre.network.provider)); const apiKit = new SafeApiKit({ chainId: toBigInt(chainId), txServiceUrl: opts.txServiceUrl, @@ -122,7 +122,7 @@ async function proposeSafeTx( return safeTxHash; } -async function waitUntilSignedAndExecuted( +export async function waitUntilSignedAndExecuted( safeTxHash: string, chainId: number, opts: UpgradeOptions & EthersDeployOptions & SafeGlobalDeployOptions, diff --git a/packages/plugin-hardhat/src/safeglobal/upgrade.ts b/packages/plugin-hardhat/src/safeglobal/upgrade.ts new file mode 100644 index 000000000..00f543727 --- /dev/null +++ b/packages/plugin-hardhat/src/safeglobal/upgrade.ts @@ -0,0 +1,73 @@ +import { Interface, TransactionResponse } from 'ethers'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { getChainId } from '@openzeppelin/upgrades-core'; +import { MetaTransactionData, OperationType } from '@safe-global/safe-core-sdk-types'; +import { proposeSafeTx, waitUntilSignedAndExecuted } from './deploy'; +import { UpgradeProxyOptions } from '../utils'; + +export async function safeGlobalAdminUpgradeAndCall( + hre: HardhatRuntimeEnvironment, + opts: UpgradeProxyOptions, + adminAddress: string, + proxyAddress: string, + nextImpl: string, + call: string, +): Promise { + console.log( + `Sending upgradeAndCall tx to ${adminAddress} with proxy:${proxyAddress} nextImpl:${nextImpl} and call:${call}`, + ); + const iface = new Interface(['function upgradeAndCall(address proxy, address implementation, bytes memory data)']); + const upgradeAndCallData = iface.encodeFunctionData('upgradeAndCall', [proxyAddress, nextImpl, call]); + const deployTxHash = await proposeAndWaitForSafeTx(hre, opts, adminAddress, upgradeAndCallData); + + const tx = await hre.ethers.provider.getTransaction(deployTxHash); + return tx ?? getNullTransactionResponse(hre); +} + +async function proposeAndWaitForSafeTx( + hre: HardhatRuntimeEnvironment, + opts: UpgradeProxyOptions, + adminAddress: string, + callData: string, +) { + const metaTxData: MetaTransactionData = { + to: adminAddress, + data: callData, + value: '0', + operation: OperationType.Call, + }; + + const safeTxHash = await proposeSafeTx(hre, metaTxData, opts); + console.log(`Safe tx hash: ${safeTxHash}`); + return await waitUntilSignedAndExecuted( + safeTxHash, + hre.network.config.chainId ?? (await getChainId(hre.ethers.provider)), + opts, + ); +} + +async function getNullTransactionResponse(hre: HardhatRuntimeEnvironment): Promise { + return new TransactionResponse( + { + blockNumber: 0, + blockHash: '', + hash: '', + index: 0, + from: '', + to: '', + data: '', + value: BigInt(0), + gasLimit: BigInt(0), + gasPrice: BigInt(0), + nonce: 0, + chainId: BigInt(hre.network.config.chainId ?? (await getChainId(hre.ethers.provider))), + type: 0, + maxPriorityFeePerGas: BigInt(0), + maxFeePerGas: BigInt(0), + signature: new hre.ethers.Signature('', '', '', 28), + accessList: [], + }, + hre.ethers.provider, + ); +} diff --git a/packages/plugin-hardhat/src/upgrade-proxy.ts b/packages/plugin-hardhat/src/upgrade-proxy.ts index 0bf2acde9..6389d7f23 100644 --- a/packages/plugin-hardhat/src/upgrade-proxy.ts +++ b/packages/plugin-hardhat/src/upgrade-proxy.ts @@ -18,6 +18,7 @@ import { attachProxyAdminV4, attachProxyAdminV5, } from './utils/attach-abi'; +import { safeGlobalAdminUpgradeAndCall } from './safeglobal/upgrade'; export type UpgradeFunction = ( proxy: ContractAddressOrInstance, @@ -83,6 +84,10 @@ export function makeUpgradeProxy( const upgradeInterfaceVersion = await getUpgradeInterfaceVersion(provider, adminAddress, log); switch (upgradeInterfaceVersion) { case '5.0.0': { + if (opts.useSafeGlobalDeploy) { + return (nextImpl, call) => + safeGlobalAdminUpgradeAndCall(hre, opts, adminAddress, proxyAddress, nextImpl, call ?? '0x'); + } const admin = await attachProxyAdminV5(hre, adminAddress, signer); return (nextImpl, call) => admin.upgradeAndCall(proxyAddress, nextImpl, call ?? '0x', ...overrides); } From 8d37bc447f32b86731a38cc13a1f249434bd0793 Mon Sep 17 00:00:00 2001 From: Naman Attri <50364269+namanB8@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:39:04 +0530 Subject: [PATCH 04/13] adds v4 methods for admin upgrades via safe global --- .../plugin-hardhat/src/safeglobal/upgrade.ts | 29 ++++++++++++++++++- packages/plugin-hardhat/src/upgrade-proxy.ts | 10 +++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/plugin-hardhat/src/safeglobal/upgrade.ts b/packages/plugin-hardhat/src/safeglobal/upgrade.ts index 00f543727..3474ecdee 100644 --- a/packages/plugin-hardhat/src/safeglobal/upgrade.ts +++ b/packages/plugin-hardhat/src/safeglobal/upgrade.ts @@ -6,7 +6,7 @@ import { MetaTransactionData, OperationType } from '@safe-global/safe-core-sdk-t import { proposeSafeTx, waitUntilSignedAndExecuted } from './deploy'; import { UpgradeProxyOptions } from '../utils'; -export async function safeGlobalAdminUpgradeAndCall( +export async function safeGlobalAdminUpgradeAndCallV5( hre: HardhatRuntimeEnvironment, opts: UpgradeProxyOptions, adminAddress: string, @@ -25,6 +25,33 @@ export async function safeGlobalAdminUpgradeAndCall( return tx ?? getNullTransactionResponse(hre); } +export async function safeGlobalAdminUpgradeV4( + hre: HardhatRuntimeEnvironment, + opts: UpgradeProxyOptions, + adminAddress: string, + proxyAddress: string, + nextImpl: string, +): Promise { + console.log(`Sending upgrade tx to ${adminAddress} with proxy:${proxyAddress} nextImpl:${nextImpl}`); + const iface = new Interface(['function upgrade(address proxy, address implementation)']); + const upgradeData = iface.encodeFunctionData('upgrade', [proxyAddress, nextImpl]); + const deployTxHash = await proposeAndWaitForSafeTx(hre, opts, adminAddress, upgradeData); + + const tx = await hre.ethers.provider.getTransaction(deployTxHash); + return tx ?? getNullTransactionResponse(hre); +} + +export async function safeGlobalAdminUpgradeAndCallV4( + hre: HardhatRuntimeEnvironment, + opts: UpgradeProxyOptions, + adminAddress: string, + proxyAddress: string, + nextImpl: string, + call: string, +): Promise { + return safeGlobalAdminUpgradeAndCallV5(hre, opts, adminAddress, proxyAddress, nextImpl, call); +} + async function proposeAndWaitForSafeTx( hre: HardhatRuntimeEnvironment, opts: UpgradeProxyOptions, diff --git a/packages/plugin-hardhat/src/upgrade-proxy.ts b/packages/plugin-hardhat/src/upgrade-proxy.ts index 6389d7f23..04a7be288 100644 --- a/packages/plugin-hardhat/src/upgrade-proxy.ts +++ b/packages/plugin-hardhat/src/upgrade-proxy.ts @@ -18,7 +18,7 @@ import { attachProxyAdminV4, attachProxyAdminV5, } from './utils/attach-abi'; -import { safeGlobalAdminUpgradeAndCall } from './safeglobal/upgrade'; +import { safeGlobalAdminUpgradeAndCallV4, safeGlobalAdminUpgradeAndCallV5, safeGlobalAdminUpgradeV4 } from './safeglobal/upgrade'; export type UpgradeFunction = ( proxy: ContractAddressOrInstance, @@ -86,12 +86,18 @@ export function makeUpgradeProxy( case '5.0.0': { if (opts.useSafeGlobalDeploy) { return (nextImpl, call) => - safeGlobalAdminUpgradeAndCall(hre, opts, adminAddress, proxyAddress, nextImpl, call ?? '0x'); + safeGlobalAdminUpgradeAndCallV5(hre, opts, adminAddress, proxyAddress, nextImpl, call ?? '0x'); } const admin = await attachProxyAdminV5(hre, adminAddress, signer); return (nextImpl, call) => admin.upgradeAndCall(proxyAddress, nextImpl, call ?? '0x', ...overrides); } default: { + if (opts.useSafeGlobalDeploy) { + return (nextImpl, call) => + call + ? safeGlobalAdminUpgradeAndCallV4(hre, opts, adminAddress, proxyAddress, nextImpl, call) + : safeGlobalAdminUpgradeV4(hre, opts, adminAddress, proxyAddress, nextImpl); + } if (upgradeInterfaceVersion !== undefined) { // Log as debug if the interface version is an unknown string. // Do not throw an error because this could be caused by a fallback function. From 931410cac5b6912fb1d453a9ba8254d5139c6350 Mon Sep 17 00:00:00 2001 From: Naman Attri <50364269+namanB8@users.noreply.github.com> Date: Mon, 22 Jul 2024 12:17:11 +0530 Subject: [PATCH 05/13] adds safe global upgrades calls for uups --- .../plugin-hardhat/src/safeglobal/upgrade.ts | 45 ++++++++++++++++++- packages/plugin-hardhat/src/upgrade-proxy.ts | 30 ++++++++++--- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/packages/plugin-hardhat/src/safeglobal/upgrade.ts b/packages/plugin-hardhat/src/safeglobal/upgrade.ts index 3474ecdee..cd230af7d 100644 --- a/packages/plugin-hardhat/src/safeglobal/upgrade.ts +++ b/packages/plugin-hardhat/src/safeglobal/upgrade.ts @@ -6,6 +6,47 @@ import { MetaTransactionData, OperationType } from '@safe-global/safe-core-sdk-t import { proposeSafeTx, waitUntilSignedAndExecuted } from './deploy'; import { UpgradeProxyOptions } from '../utils'; +export async function safeGlobalUpgradeToAndCallV5( + hre: HardhatRuntimeEnvironment, + opts: UpgradeProxyOptions, + proxyAddress: string, + nextImpl: string, + call: string, +): Promise { + console.log(`Sending upgradeToAndCall tx to proxy:${proxyAddress} with nextImpl:${nextImpl} and call:${call}`); + const iface = new Interface(['function upgradeToAndCall(address newImplementation, bytes memory data)']); + const callData = iface.encodeFunctionData('upgradeToAndCall', [nextImpl, call]); + const deployTxHash = await proposeAndWaitForSafeTx(hre, opts, proxyAddress, callData); + + const tx = await hre.ethers.provider.getTransaction(deployTxHash); + return tx ?? getNullTransactionResponse(hre); +} + +export async function safeGlobalUpgradeToV4( + hre: HardhatRuntimeEnvironment, + opts: UpgradeProxyOptions, + proxyAddress: string, + nextImpl: string, +): Promise { + console.log(`Sending upgradeTo tx to proxy:${proxyAddress} with nextImpl:${nextImpl}`); + const iface = new Interface(['function upgradeTo(address newImplementation)']); + const callData = iface.encodeFunctionData('upgradeTo', [nextImpl]); + const deployTxHash = await proposeAndWaitForSafeTx(hre, opts, proxyAddress, callData); + + const tx = await hre.ethers.provider.getTransaction(deployTxHash); + return tx ?? getNullTransactionResponse(hre); +} + +export async function safeGlobalUpgradeToAndCallV4( + hre: HardhatRuntimeEnvironment, + opts: UpgradeProxyOptions, + proxyAddress: string, + nextImpl: string, + call: string, +): Promise { + return safeGlobalUpgradeToAndCallV5(hre, opts, proxyAddress, nextImpl, call); +} + export async function safeGlobalAdminUpgradeAndCallV5( hre: HardhatRuntimeEnvironment, opts: UpgradeProxyOptions, @@ -55,11 +96,11 @@ export async function safeGlobalAdminUpgradeAndCallV4( async function proposeAndWaitForSafeTx( hre: HardhatRuntimeEnvironment, opts: UpgradeProxyOptions, - adminAddress: string, + to: string, callData: string, ) { const metaTxData: MetaTransactionData = { - to: adminAddress, + to, data: callData, value: '0', operation: OperationType.Call, diff --git a/packages/plugin-hardhat/src/upgrade-proxy.ts b/packages/plugin-hardhat/src/upgrade-proxy.ts index 04a7be288..d218beb42 100644 --- a/packages/plugin-hardhat/src/upgrade-proxy.ts +++ b/packages/plugin-hardhat/src/upgrade-proxy.ts @@ -18,7 +18,14 @@ import { attachProxyAdminV4, attachProxyAdminV5, } from './utils/attach-abi'; -import { safeGlobalAdminUpgradeAndCallV4, safeGlobalAdminUpgradeAndCallV5, safeGlobalAdminUpgradeV4 } from './safeglobal/upgrade'; +import { + safeGlobalAdminUpgradeAndCallV4, + safeGlobalAdminUpgradeAndCallV5, + safeGlobalAdminUpgradeV4, + safeGlobalUpgradeToAndCallV4, + safeGlobalUpgradeToAndCallV5, + safeGlobalUpgradeToV4, +} from './safeglobal/upgrade'; export type UpgradeFunction = ( proxy: ContractAddressOrInstance, @@ -63,6 +70,9 @@ export function makeUpgradeProxy( const upgradeInterfaceVersion = await getUpgradeInterfaceVersion(provider, proxyAddress, log); switch (upgradeInterfaceVersion) { case '5.0.0': { + if (opts.useSafeGlobalDeploy) { + return (nextImpl, call) => safeGlobalUpgradeToAndCallV5(hre, opts, proxyAddress, nextImpl, call ?? '0x'); + } const proxy = await attachITransparentUpgradeableProxyV5(hre, proxyAddress, signer); return (nextImpl, call) => proxy.upgradeToAndCall(nextImpl, call ?? '0x', ...overrides); } @@ -74,6 +84,12 @@ export function makeUpgradeProxy( `Unknown UPGRADE_INTERFACE_VERSION ${upgradeInterfaceVersion} for proxy at ${proxyAddress}. Expected 5.0.0`, ); } + if (opts.useSafeGlobalDeploy) { + return (nextImpl, call) => + call + ? safeGlobalUpgradeToAndCallV4(hre, opts, proxyAddress, nextImpl, call) + : safeGlobalUpgradeToV4(hre, opts, proxyAddress, nextImpl); + } const proxy = await attachITransparentUpgradeableProxyV4(hre, proxyAddress, signer); return (nextImpl, call) => call ? proxy.upgradeToAndCall(nextImpl, call, ...overrides) : proxy.upgradeTo(nextImpl, ...overrides); @@ -92,12 +108,6 @@ export function makeUpgradeProxy( return (nextImpl, call) => admin.upgradeAndCall(proxyAddress, nextImpl, call ?? '0x', ...overrides); } default: { - if (opts.useSafeGlobalDeploy) { - return (nextImpl, call) => - call - ? safeGlobalAdminUpgradeAndCallV4(hre, opts, adminAddress, proxyAddress, nextImpl, call) - : safeGlobalAdminUpgradeV4(hre, opts, adminAddress, proxyAddress, nextImpl); - } if (upgradeInterfaceVersion !== undefined) { // Log as debug if the interface version is an unknown string. // Do not throw an error because this could be caused by a fallback function. @@ -105,6 +115,12 @@ export function makeUpgradeProxy( `Unknown UPGRADE_INTERFACE_VERSION ${upgradeInterfaceVersion} for proxy admin at ${adminAddress}. Expected 5.0.0`, ); } + if (opts.useSafeGlobalDeploy) { + return (nextImpl, call) => + call + ? safeGlobalAdminUpgradeAndCallV4(hre, opts, adminAddress, proxyAddress, nextImpl, call) + : safeGlobalAdminUpgradeV4(hre, opts, adminAddress, proxyAddress, nextImpl); + } const admin = await attachProxyAdminV4(hre, adminAddress, signer); return (nextImpl, call) => call From 79a564478bb54994bd0c4e0fe4046789649b79e3 Mon Sep 17 00:00:00 2001 From: Naman Attri <50364269+namanB8@users.noreply.github.com> Date: Mon, 22 Jul 2024 15:13:21 +0530 Subject: [PATCH 06/13] upgrade beacon using safe global --- packages/plugin-hardhat/src/safeglobal/upgrade.ts | 15 +++++++++++++++ packages/plugin-hardhat/src/upgrade-beacon.ts | 5 ++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/plugin-hardhat/src/safeglobal/upgrade.ts b/packages/plugin-hardhat/src/safeglobal/upgrade.ts index cd230af7d..42aff6fa0 100644 --- a/packages/plugin-hardhat/src/safeglobal/upgrade.ts +++ b/packages/plugin-hardhat/src/safeglobal/upgrade.ts @@ -93,6 +93,21 @@ export async function safeGlobalAdminUpgradeAndCallV4( return safeGlobalAdminUpgradeAndCallV5(hre, opts, adminAddress, proxyAddress, nextImpl, call); } +export async function safeGlobalBeaconUpgradeTo( + hre: HardhatRuntimeEnvironment, + opts: UpgradeProxyOptions, + beaconAddress: string, + nextImpl: string, +): Promise { + console.log(`Sending upgradeTo tx to beacon:${beaconAddress} with nextImpl:${nextImpl}`); + const iface = new Interface(['function upgradeTo(address newImplementation)']); + const callData = iface.encodeFunctionData('upgradeTo', [nextImpl]); + const deployTxHash = await proposeAndWaitForSafeTx(hre, opts, beaconAddress, callData); + + const tx = await hre.ethers.provider.getTransaction(deployTxHash); + return tx ?? getNullTransactionResponse(hre); +} + async function proposeAndWaitForSafeTx( hre: HardhatRuntimeEnvironment, opts: UpgradeProxyOptions, diff --git a/packages/plugin-hardhat/src/upgrade-beacon.ts b/packages/plugin-hardhat/src/upgrade-beacon.ts index 980d80e73..0d215b05b 100644 --- a/packages/plugin-hardhat/src/upgrade-beacon.ts +++ b/packages/plugin-hardhat/src/upgrade-beacon.ts @@ -11,6 +11,7 @@ import { getSigner, } from './utils'; import { disableDefender } from './defender/utils'; +import { safeGlobalBeaconUpgradeTo } from './safeglobal/upgrade'; export type UpgradeBeaconFunction = ( beacon: ContractAddressOrInstance, @@ -29,7 +30,9 @@ export function makeUpgradeBeacon(hre: HardhatRuntimeEnvironment, defenderModule const beaconContract = attach(UpgradeableBeaconFactory, beaconAddress); const overrides = opts.txOverrides ? [opts.txOverrides] : []; - const upgradeTx = await beaconContract.upgradeTo(nextImpl, ...overrides); + const upgradeTx = opts.useSafeGlobalDeploy + ? await safeGlobalBeaconUpgradeTo(hre, opts, beaconAddress, nextImpl) + : await beaconContract.upgradeTo(nextImpl, ...overrides); // @ts-ignore Won't be readonly because beaconContract was created through attach. beaconContract.deployTransaction = upgradeTx; From 21c695c5f5561b1f65d471fa80e67f28497b8c57 Mon Sep 17 00:00:00 2001 From: Naman Attri <50364269+namanB8@users.noreply.github.com> Date: Mon, 22 Jul 2024 16:00:12 +0530 Subject: [PATCH 07/13] adds safe global support for transfer admin & transfer admin ownership --- packages/plugin-hardhat/src/admin.ts | 30 +++++++++------ .../plugin-hardhat/src/safeglobal/admin.ts | 38 +++++++++++++++++++ .../plugin-hardhat/src/safeglobal/upgrade.ts | 4 +- 3 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 packages/plugin-hardhat/src/safeglobal/admin.ts diff --git a/packages/plugin-hardhat/src/admin.ts b/packages/plugin-hardhat/src/admin.ts index 6b8abeb52..0470479c2 100644 --- a/packages/plugin-hardhat/src/admin.ts +++ b/packages/plugin-hardhat/src/admin.ts @@ -2,8 +2,9 @@ import chalk from 'chalk'; import type { HardhatRuntimeEnvironment } from 'hardhat/types'; import { Manifest, getAdminAddress } from '@openzeppelin/upgrades-core'; import { Contract, Signer } from 'ethers'; -import { EthersDeployOptions, attachProxyAdminV4 } from './utils'; +import { EthersDeployOptions, SafeGlobalDeployOptions, attachProxyAdminV4 } from './utils'; import { disableDefender } from './defender/utils'; +import { safeGlobalAdminChangeProxyAdminV4, safeGlobalAdminTransferOwnership } from './safeglobal/admin'; const SUCCESS_CHECK = chalk.green('✔') + ' '; @@ -15,13 +16,13 @@ export type ChangeAdminFunction = ( proxyAddress: string, newAdmin: string, signer?: Signer, - opts?: EthersDeployOptions, + opts?: EthersDeployOptions & SafeGlobalDeployOptions, ) => Promise; export type TransferProxyAdminOwnershipFunction = ( proxyAddress: string, newOwner: string, signer?: Signer, - opts?: TransferProxyAdminOwnershipOptions & EthersDeployOptions, + opts?: TransferProxyAdminOwnershipOptions & EthersDeployOptions & SafeGlobalDeployOptions, ) => Promise; export type GetInstanceFunction = (signer?: Signer) => Promise; @@ -30,16 +31,20 @@ export function makeChangeProxyAdmin(hre: HardhatRuntimeEnvironment, defenderMod proxyAddress: string, newAdmin: string, signer?: Signer, - opts: EthersDeployOptions = {}, + opts: EthersDeployOptions & SafeGlobalDeployOptions = {}, ) { disableDefender(hre, defenderModule, {}, changeProxyAdmin.name); const proxyAdminAddress = await getAdminAddress(hre.network.provider, proxyAddress); // Only compatible with v4 admins - const admin = await attachProxyAdminV4(hre, proxyAdminAddress, signer); + if (opts.useSafeGlobalDeploy) { + await safeGlobalAdminChangeProxyAdminV4(hre, opts, proxyAdminAddress, proxyAddress, newAdmin); + } else { + const admin = await attachProxyAdminV4(hre, proxyAdminAddress, signer); - const overrides = opts.txOverrides ? [opts.txOverrides] : []; - await admin.changeProxyAdmin(proxyAddress, newAdmin, ...overrides); + const overrides = opts.txOverrides ? [opts.txOverrides] : []; + await admin.changeProxyAdmin(proxyAddress, newAdmin, ...overrides); + } }; } @@ -51,16 +56,19 @@ export function makeTransferProxyAdminOwnership( proxyAddress: string, newOwner: string, signer?: Signer, - opts: TransferProxyAdminOwnershipOptions & EthersDeployOptions = {}, + opts: TransferProxyAdminOwnershipOptions & EthersDeployOptions & SafeGlobalDeployOptions = {}, ) { disableDefender(hre, defenderModule, {}, transferProxyAdminOwnership.name); const proxyAdminAddress = await getAdminAddress(hre.network.provider, proxyAddress); // Compatible with both v4 and v5 admins since they both have transferOwnership const admin = await attachProxyAdminV4(hre, proxyAdminAddress, signer); - - const overrides = opts.txOverrides ? [opts.txOverrides] : []; - await admin.transferOwnership(newOwner, ...overrides); + if (opts.useSafeGlobalDeploy) { + await safeGlobalAdminTransferOwnership(hre, opts, proxyAdminAddress, newOwner); + } else { + const overrides = opts.txOverrides ? [opts.txOverrides] : []; + await admin.transferOwnership(newOwner, ...overrides); + } if (!opts.silent) { const { provider } = hre.network; diff --git a/packages/plugin-hardhat/src/safeglobal/admin.ts b/packages/plugin-hardhat/src/safeglobal/admin.ts new file mode 100644 index 000000000..c39843224 --- /dev/null +++ b/packages/plugin-hardhat/src/safeglobal/admin.ts @@ -0,0 +1,38 @@ +import { Interface, TransactionResponse } from 'ethers'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { UpgradeProxyOptions } from '../utils'; +import { getNullTransactionResponse, proposeAndWaitForSafeTx } from './upgrade'; + +export async function safeGlobalAdminChangeProxyAdminV4( + hre: HardhatRuntimeEnvironment, + opts: UpgradeProxyOptions, + adminAddress: string, + proxyAddress: string, + newAdmin: string, +): Promise { + console.log( + `Sending changeProxyAdmin tx to admin:${adminAddress} with proxy:${proxyAddress} and nextImpl:${newAdmin}`, + ); + const iface = new Interface(['function changeProxyAdmin(address proxy, address newAdmin)']); + const callData = iface.encodeFunctionData('changeProxyAdmin', [proxyAddress, newAdmin]); + const deployTxHash = await proposeAndWaitForSafeTx(hre, opts, adminAddress, callData); + + const tx = await hre.ethers.provider.getTransaction(deployTxHash); + return tx ?? getNullTransactionResponse(hre); +} + +export async function safeGlobalAdminTransferOwnership( + hre: HardhatRuntimeEnvironment, + opts: UpgradeProxyOptions, + adminAddress: string, + newOwner: string, +): Promise { + console.log(`Sending transferOwnership tx to admin:${adminAddress} with newOwner:${newOwner}`); + const iface = new Interface(['function transferOwnership(address newOwner)']); + const callData = iface.encodeFunctionData('transferOwnership', [newOwner]); + const deployTxHash = await proposeAndWaitForSafeTx(hre, opts, adminAddress, callData); + + const tx = await hre.ethers.provider.getTransaction(deployTxHash); + return tx ?? getNullTransactionResponse(hre); +} diff --git a/packages/plugin-hardhat/src/safeglobal/upgrade.ts b/packages/plugin-hardhat/src/safeglobal/upgrade.ts index 42aff6fa0..6ea5880da 100644 --- a/packages/plugin-hardhat/src/safeglobal/upgrade.ts +++ b/packages/plugin-hardhat/src/safeglobal/upgrade.ts @@ -108,7 +108,7 @@ export async function safeGlobalBeaconUpgradeTo( return tx ?? getNullTransactionResponse(hre); } -async function proposeAndWaitForSafeTx( +export async function proposeAndWaitForSafeTx( hre: HardhatRuntimeEnvironment, opts: UpgradeProxyOptions, to: string, @@ -130,7 +130,7 @@ async function proposeAndWaitForSafeTx( ); } -async function getNullTransactionResponse(hre: HardhatRuntimeEnvironment): Promise { +export async function getNullTransactionResponse(hre: HardhatRuntimeEnvironment): Promise { return new TransactionResponse( { blockNumber: 0, From 4f760baa5c7cd992c652350cb17431341548eb8a Mon Sep 17 00:00:00 2001 From: Naman Attri <50364269+namanB8@users.noreply.github.com> Date: Mon, 22 Jul 2024 16:18:27 +0530 Subject: [PATCH 08/13] removes the signer flag from protocol kit init --- packages/plugin-hardhat/src/safeglobal/deploy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/plugin-hardhat/src/safeglobal/deploy.ts b/packages/plugin-hardhat/src/safeglobal/deploy.ts index acb9d886d..2b6273a07 100644 --- a/packages/plugin-hardhat/src/safeglobal/deploy.ts +++ b/packages/plugin-hardhat/src/safeglobal/deploy.ts @@ -85,7 +85,6 @@ export async function proposeSafeTx( const protocolKitOwner1 = await Safe.init({ provider: hre.network.provider, - signer: process.env.SIGNER, safeAddress: opts.safeAddress ?? '', contractNetworks: { [chainId]: { From a3427e30a29b2eff00c2ac5cb004506e87d18298 Mon Sep 17 00:00:00 2001 From: Naman Attri <50364269+namanB8@users.noreply.github.com> Date: Tue, 23 Jul 2024 23:26:14 +0530 Subject: [PATCH 09/13] simplifies create2 interface --- packages/core/contracts/CreateCall.sol | 48 ------------------- .../plugin-hardhat/src/safeglobal/deploy.ts | 7 ++- 2 files changed, 3 insertions(+), 52 deletions(-) delete mode 100644 packages/core/contracts/CreateCall.sol diff --git a/packages/core/contracts/CreateCall.sol b/packages/core/contracts/CreateCall.sol deleted file mode 100644 index 1a7988d84..000000000 --- a/packages/core/contracts/CreateCall.sol +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only - -pragma solidity >=0.7.0 <0.9.0; - -/** - * @title Create Call - Allows to use the different create opcodes to deploy a contract. - * @author Richard Meissner - @rmeissner - * @notice This contract provides functions for deploying a new contract using the create and create2 opcodes. - */ -contract CreateCall { - /// @notice Emitted when a new contract is created - event ContractCreation(address indexed newContract); - - /** - * @notice Deploys a new contract using the create2 opcode. - * @param value The value in wei to be sent with the contract creation. - * @param deploymentData The initialisation code of the contract to be created. - * @param salt The salt value to use for the contract creation. - * @return newContract The address of the newly created contract. - */ - function performCreate2(uint256 value, bytes memory deploymentData, bytes32 salt) public returns (address newContract) { - /* solhint-disable no-inline-assembly */ - /// @solidity memory-safe-assembly - assembly { - newContract := create2(value, add(0x20, deploymentData), mload(deploymentData), salt) - } - /* solhint-enable no-inline-assembly */ - require(newContract != address(0), "Could not deploy contract"); - emit ContractCreation(newContract); - } - - /** - * @notice Deploys a new contract using the create opcode. - * @param value The value in wei to be sent with the contract creation. - * @param deploymentData The initialisation code of the contract to be created. - * @return newContract The address of the newly created contract. - */ - function performCreate(uint256 value, bytes memory deploymentData) public returns (address newContract) { - /* solhint-disable no-inline-assembly */ - /// @solidity memory-safe-assembly - assembly { - newContract := create(value, add(deploymentData, 0x20), mload(deploymentData)) - } - /* solhint-enable no-inline-assembly */ - require(newContract != address(0), "Could not deploy contract"); - emit ContractCreation(newContract); - } -} \ No newline at end of file diff --git a/packages/plugin-hardhat/src/safeglobal/deploy.ts b/packages/plugin-hardhat/src/safeglobal/deploy.ts index 2b6273a07..fb429d593 100644 --- a/packages/plugin-hardhat/src/safeglobal/deploy.ts +++ b/packages/plugin-hardhat/src/safeglobal/deploy.ts @@ -6,8 +6,6 @@ import { MetaTransactionData, OperationType } from '@safe-global/safe-core-sdk-t import SafeApiKit from '@safe-global/api-kit'; import Safe from '@safe-global/protocol-kit'; -import CreateCall from '@openzeppelin/upgrades-core/artifacts/contracts/CreateCall.sol/CreateCall.json'; - import { DeployTransaction, UpgradeOptions, EthersDeployOptions, SafeGlobalDeployOptions } from '../utils'; export async function safeGlobalDeploy( @@ -23,6 +21,7 @@ export async function safeGlobalDeploy( const create2Data = await getPerformCreate2Data(tx.data, opts); console.log('Proposing multisig deployment tx and waiting for contract to be deployed...'); const deployTxHash = await proposeAndWaitForSafeTx(hre, create2Data, chainId, opts); + console.log('Getting deployed contract address...'); const [address, txResponse] = await getCreate2DeployedContractAddress(hre, deployTxHash); const deployTransaction = txResponse; @@ -46,7 +45,7 @@ async function getPerformCreate2Data( if (opts.salt === undefined || opts.salt === '' || opts.salt.trim() === '') { throw new Error('Salt must be provided for create2 deployment'); } - const iface = new Interface(CreateCall.abi); + const iface = new Interface(['function performCreate2(uint256 value, bytes deploymentData, bytes32 salt)']); const performCreate2Data = iface.encodeFunctionData('performCreate2', [0, deployData, id(opts.salt)]); return performCreate2Data; } @@ -151,7 +150,7 @@ async function getCreate2DeployedContractAddress( hre: HardhatRuntimeEnvironment, txHash: string, ): Promise<[string, TransactionResponse | null, TransactionReceipt | null]> { - const iface = new Interface(CreateCall.abi); + const iface = new Interface(['event ContractCreation(address newContract)']); const provider = hre.ethers.provider; const tx = await provider.getTransaction(txHash); const receipt = await provider.getTransactionReceipt(txHash); From 535507a5ad490dab73fea36f27cb6a378ea33934 Mon Sep 17 00:00:00 2001 From: Naman Attri <50364269+namanB8@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:33:39 +0530 Subject: [PATCH 10/13] reduces unneccesary options import --- .../plugin-hardhat/src/safeglobal/deploy.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/plugin-hardhat/src/safeglobal/deploy.ts b/packages/plugin-hardhat/src/safeglobal/deploy.ts index fb429d593..1e36fbb5d 100644 --- a/packages/plugin-hardhat/src/safeglobal/deploy.ts +++ b/packages/plugin-hardhat/src/safeglobal/deploy.ts @@ -6,12 +6,12 @@ import { MetaTransactionData, OperationType } from '@safe-global/safe-core-sdk-t import SafeApiKit from '@safe-global/api-kit'; import Safe from '@safe-global/protocol-kit'; -import { DeployTransaction, UpgradeOptions, EthersDeployOptions, SafeGlobalDeployOptions } from '../utils'; +import { DeployTransaction, DeployProxyOptions } from '../utils'; export async function safeGlobalDeploy( hre: HardhatRuntimeEnvironment, factory: ContractFactory, - opts: UpgradeOptions & EthersDeployOptions & SafeGlobalDeployOptions, + opts: DeployProxyOptions, ...args: unknown[] ): Promise & RemoteDeploymentId> { const tx = await factory.getDeployTransaction(...args); @@ -38,10 +38,7 @@ export async function safeGlobalDeploy( }; } -async function getPerformCreate2Data( - deployData: string, - opts: UpgradeOptions & EthersDeployOptions & SafeGlobalDeployOptions, -): Promise { +async function getPerformCreate2Data(deployData: string, opts: DeployProxyOptions): Promise { if (opts.salt === undefined || opts.salt === '' || opts.salt.trim() === '') { throw new Error('Salt must be provided for create2 deployment'); } @@ -54,7 +51,7 @@ async function proposeAndWaitForSafeTx( hre: HardhatRuntimeEnvironment, performCreate2Data: string, chainId: number, - opts: UpgradeOptions & EthersDeployOptions & SafeGlobalDeployOptions, + opts: DeployProxyOptions, ) { if (opts.createCallAddress === undefined || opts.createCallAddress === '') { throw new Error('CreateCall address must be provided for create2 deployment'); @@ -74,7 +71,7 @@ async function proposeAndWaitForSafeTx( export async function proposeSafeTx( hre: HardhatRuntimeEnvironment, txData: MetaTransactionData, - opts: UpgradeOptions & EthersDeployOptions & SafeGlobalDeployOptions, + opts: DeployProxyOptions, ) { const chainId = hre.network.config.chainId ?? (await getChainId(hre.network.provider)); const apiKit = new SafeApiKit({ @@ -120,11 +117,7 @@ export async function proposeSafeTx( return safeTxHash; } -export async function waitUntilSignedAndExecuted( - safeTxHash: string, - chainId: number, - opts: UpgradeOptions & EthersDeployOptions & SafeGlobalDeployOptions, -) { +export async function waitUntilSignedAndExecuted(safeTxHash: string, chainId: number, opts: DeployProxyOptions) { const apiKit = new SafeApiKit({ chainId: toBigInt(chainId), txServiceUrl: opts.txServiceUrl, From 9fb3f4f6d45e64f12bede46ef843073f9ef35615 Mon Sep 17 00:00:00 2001 From: Naman Attri <50364269+namanB8@users.noreply.github.com> Date: Wed, 24 Jul 2024 17:14:48 +0530 Subject: [PATCH 11/13] enhance reusability by removing redundant code --- .../plugin-hardhat/src/safeglobal/admin.ts | 3 +- .../plugin-hardhat/src/safeglobal/deploy.ts | 79 ++++++------------- .../plugin-hardhat/src/safeglobal/upgrade.ts | 25 +----- 3 files changed, 27 insertions(+), 80 deletions(-) diff --git a/packages/plugin-hardhat/src/safeglobal/admin.ts b/packages/plugin-hardhat/src/safeglobal/admin.ts index c39843224..9a32fa04e 100644 --- a/packages/plugin-hardhat/src/safeglobal/admin.ts +++ b/packages/plugin-hardhat/src/safeglobal/admin.ts @@ -2,7 +2,8 @@ import { Interface, TransactionResponse } from 'ethers'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { UpgradeProxyOptions } from '../utils'; -import { getNullTransactionResponse, proposeAndWaitForSafeTx } from './upgrade'; +import { getNullTransactionResponse } from './upgrade'; +import { proposeAndWaitForSafeTx } from './deploy'; export async function safeGlobalAdminChangeProxyAdminV4( hre: HardhatRuntimeEnvironment, diff --git a/packages/plugin-hardhat/src/safeglobal/deploy.ts b/packages/plugin-hardhat/src/safeglobal/deploy.ts index 1e36fbb5d..a6e52a41f 100644 --- a/packages/plugin-hardhat/src/safeglobal/deploy.ts +++ b/packages/plugin-hardhat/src/safeglobal/deploy.ts @@ -15,14 +15,13 @@ export async function safeGlobalDeploy( ...args: unknown[] ): Promise & RemoteDeploymentId> { const tx = await factory.getDeployTransaction(...args); - const chainId = hre.network.config.chainId ?? (await getChainId(hre.network.provider)); - // create performCreate2Tx - const create2Data = await getPerformCreate2Data(tx.data, opts); + const create2Data = await getCreate2CallData(tx.data, opts); console.log('Proposing multisig deployment tx and waiting for contract to be deployed...'); - const deployTxHash = await proposeAndWaitForSafeTx(hre, create2Data, chainId, opts); + const deployTxHash = await proposeAndWaitForSafeTx(hre, opts, opts.createCallAddress ?? '', create2Data); console.log('Getting deployed contract address...'); const [address, txResponse] = await getCreate2DeployedContractAddress(hre, deployTxHash); + console.log(`Contract deployed at: ${address}`); const deployTransaction = txResponse; if (deployTransaction === null) { @@ -38,7 +37,7 @@ export async function safeGlobalDeploy( }; } -async function getPerformCreate2Data(deployData: string, opts: DeployProxyOptions): Promise { +async function getCreate2CallData(deployData: string, opts: DeployProxyOptions): Promise { if (opts.salt === undefined || opts.salt === '' || opts.salt.trim() === '') { throw new Error('Salt must be provided for create2 deployment'); } @@ -47,32 +46,19 @@ async function getPerformCreate2Data(deployData: string, opts: DeployProxyOption return performCreate2Data; } -async function proposeAndWaitForSafeTx( +export async function proposeAndWaitForSafeTx( hre: HardhatRuntimeEnvironment, - performCreate2Data: string, - chainId: number, opts: DeployProxyOptions, + to: string, + callData: string, ) { - if (opts.createCallAddress === undefined || opts.createCallAddress === '') { - throw new Error('CreateCall address must be provided for create2 deployment'); - } - const performCreate2MetaTxData: MetaTransactionData = { - to: opts.createCallAddress, - data: performCreate2Data, + const metaTxData: MetaTransactionData = { + to, + data: callData, value: '0', operation: OperationType.Call, }; - const safeTxHash = await proposeSafeTx(hre, performCreate2MetaTxData, opts); - console.log(`Safe tx hash: ${safeTxHash}`); - return await waitUntilSignedAndExecuted(safeTxHash, chainId, opts); -} - -export async function proposeSafeTx( - hre: HardhatRuntimeEnvironment, - txData: MetaTransactionData, - opts: DeployProxyOptions, -) { const chainId = hre.network.config.chainId ?? (await getChainId(hre.network.provider)); const apiKit = new SafeApiKit({ chainId: toBigInt(chainId), @@ -84,50 +70,35 @@ export async function proposeSafeTx( safeAddress: opts.safeAddress ?? '', contractNetworks: { [chainId]: { - safeSingletonAddress: opts.safeSingletonAddress ?? '', - safeProxyFactoryAddress: opts.safeProxyFactoryAddress ?? '', - multiSendAddress: opts.multiSendAddress ?? '', - multiSendCallOnlyAddress: opts.multiSendCallOnlyAddress ?? '', - fallbackHandlerAddress: opts.fallbackHandlerAddress ?? '', - signMessageLibAddress: opts.signMessageLibAddress ?? '', - createCallAddress: opts.createCallAddress ?? '', - simulateTxAccessorAddress: opts.simulateTxAccessorAddress ?? '', + // default values set from: https://github.com/safe-global/safe-deployments/tree/main/src/assets/v1.4.1 + safeSingletonAddress: opts.safeSingletonAddress ?? '0x41675C099F32341bf84BFc5382aF534df5C7461a', + safeProxyFactoryAddress: opts.safeProxyFactoryAddress ?? '0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67', + multiSendAddress: opts.multiSendAddress ?? '0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526', + multiSendCallOnlyAddress: opts.multiSendCallOnlyAddress ?? '0x9641d764fc13c8B624c04430C7356C1C7C8102e2', + fallbackHandlerAddress: opts.fallbackHandlerAddress ?? '0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99', + signMessageLibAddress: opts.signMessageLibAddress ?? '0xd53cd0aB83D845Ac265BE939c57F53AD838012c9', + createCallAddress: opts.createCallAddress ?? '0x9b35Af71d77eaf8d7e40252370304687390A1A52', + simulateTxAccessorAddress: opts.simulateTxAccessorAddress ?? '0x3d4BA2E0884aa488718476ca2FB8Efc291A46199', }, }, }); - const safeAddress = await protocolKitOwner1.getAddress(); - // Sign and send the transaction // Create a Safe transaction with the provided parameters - const safeTransaction = await protocolKitOwner1.createTransaction({ transactions: [txData] }); - + const safeTransaction = await protocolKitOwner1.createTransaction({ transactions: [metaTxData] }); const safeTxHash = await protocolKitOwner1.getTransactionHash(safeTransaction); - + console.log(`Safe tx hash: ${safeTxHash}`); const senderSignature = await protocolKitOwner1.signHash(safeTxHash); await apiKit.proposeTransaction({ - safeAddress, + safeAddress: opts.safeAddress ?? '', safeTransactionData: safeTransaction.data, safeTxHash, - senderAddress: '0x25843121E84f6E52a140885986FD890d302c34EE', + senderAddress: senderSignature.signer, senderSignature: senderSignature.data, }); - return safeTxHash; -} - -export async function waitUntilSignedAndExecuted(safeTxHash: string, chainId: number, opts: DeployProxyOptions) { - const apiKit = new SafeApiKit({ - chainId: toBigInt(chainId), - txServiceUrl: opts.txServiceUrl, - }); - - const safeTx = await apiKit.getTransaction(safeTxHash); - if (safeTx.isExecuted) { - return safeTx.transactionHash; - } - + // wait until tx is signed & executed return new Promise(resolve => { const interval = setInterval(async () => { const safeTx = await apiKit.getTransaction(safeTxHash); @@ -158,8 +129,6 @@ async function getCreate2DeployedContractAddress( try { const parsedLog = iface.parseLog(log); if (parsedLog?.name === 'ContractCreation') { - console.log(`Event: ${parsedLog?.name}`); - console.log(`New Contract: ${parsedLog?.args.newContract}`); return [parsedLog?.args.newContract, tx, receipt]; } } catch (error) { diff --git a/packages/plugin-hardhat/src/safeglobal/upgrade.ts b/packages/plugin-hardhat/src/safeglobal/upgrade.ts index 6ea5880da..fb45919b1 100644 --- a/packages/plugin-hardhat/src/safeglobal/upgrade.ts +++ b/packages/plugin-hardhat/src/safeglobal/upgrade.ts @@ -2,8 +2,7 @@ import { Interface, TransactionResponse } from 'ethers'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { getChainId } from '@openzeppelin/upgrades-core'; -import { MetaTransactionData, OperationType } from '@safe-global/safe-core-sdk-types'; -import { proposeSafeTx, waitUntilSignedAndExecuted } from './deploy'; +import { proposeAndWaitForSafeTx } from './deploy'; import { UpgradeProxyOptions } from '../utils'; export async function safeGlobalUpgradeToAndCallV5( @@ -108,28 +107,6 @@ export async function safeGlobalBeaconUpgradeTo( return tx ?? getNullTransactionResponse(hre); } -export async function proposeAndWaitForSafeTx( - hre: HardhatRuntimeEnvironment, - opts: UpgradeProxyOptions, - to: string, - callData: string, -) { - const metaTxData: MetaTransactionData = { - to, - data: callData, - value: '0', - operation: OperationType.Call, - }; - - const safeTxHash = await proposeSafeTx(hre, metaTxData, opts); - console.log(`Safe tx hash: ${safeTxHash}`); - return await waitUntilSignedAndExecuted( - safeTxHash, - hre.network.config.chainId ?? (await getChainId(hre.ethers.provider)), - opts, - ); -} - export async function getNullTransactionResponse(hre: HardhatRuntimeEnvironment): Promise { return new TransactionResponse( { From c404edc752a106f10ae44fe09b7417cc751a0b0d Mon Sep 17 00:00:00 2001 From: Naman Attri <50364269+namanB8@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:06:42 +0530 Subject: [PATCH 12/13] adds default tx service api url --- packages/plugin-hardhat/src/safeglobal/deploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-hardhat/src/safeglobal/deploy.ts b/packages/plugin-hardhat/src/safeglobal/deploy.ts index a6e52a41f..404e10561 100644 --- a/packages/plugin-hardhat/src/safeglobal/deploy.ts +++ b/packages/plugin-hardhat/src/safeglobal/deploy.ts @@ -62,7 +62,7 @@ export async function proposeAndWaitForSafeTx( const chainId = hre.network.config.chainId ?? (await getChainId(hre.network.provider)); const apiKit = new SafeApiKit({ chainId: toBigInt(chainId), - txServiceUrl: opts.txServiceUrl, + txServiceUrl: opts.txServiceUrl ?? 'https://safe-transaction-mainnet.safe.global/api', }); const protocolKitOwner1 = await Safe.init({ From 53791604000b0603cc83d746ae75ffd27bc33e73 Mon Sep 17 00:00:00 2001 From: Naman Attri <50364269+namanB8@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:11:22 +0530 Subject: [PATCH 13/13] fixes comment in safe global options --- packages/plugin-hardhat/src/utils/options.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-hardhat/src/utils/options.ts b/packages/plugin-hardhat/src/utils/options.ts index 434b90c42..91b977f4f 100644 --- a/packages/plugin-hardhat/src/utils/options.ts +++ b/packages/plugin-hardhat/src/utils/options.ts @@ -77,7 +77,7 @@ export type SafeGlobalDeploy = { }; /** - * Options for functions that support Defender deployments. + * Options for functions that support SafeGlobal deployments. */ export type SafeGlobalDeployOptions = SafeGlobalDeploy & { txServiceUrl?: string;