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", 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..9a32fa04e --- /dev/null +++ b/packages/plugin-hardhat/src/safeglobal/admin.ts @@ -0,0 +1,39 @@ +import { Interface, TransactionResponse } from 'ethers'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { UpgradeProxyOptions } from '../utils'; +import { getNullTransactionResponse } from './upgrade'; +import { proposeAndWaitForSafeTx } from './deploy'; + +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/deploy.ts b/packages/plugin-hardhat/src/safeglobal/deploy.ts new file mode 100644 index 000000000..404e10561 --- /dev/null +++ b/packages/plugin-hardhat/src/safeglobal/deploy.ts @@ -0,0 +1,139 @@ +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 { DeployTransaction, DeployProxyOptions } from '../utils'; + +export async function safeGlobalDeploy( + hre: HardhatRuntimeEnvironment, + factory: ContractFactory, + opts: DeployProxyOptions, + ...args: unknown[] +): Promise & RemoteDeploymentId> { + const tx = await factory.getDeployTransaction(...args); + + 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, 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) { + throw new Error('Broken invariant: deploymentTransaction is null'); + } + + const txHash = deployTransaction.hash; + + return { + address, + txHash, + deployTransaction, + }; +} + +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'); + } + const iface = new Interface(['function performCreate2(uint256 value, bytes deploymentData, bytes32 salt)']); + const performCreate2Data = iface.encodeFunctionData('performCreate2', [0, deployData, id(opts.salt)]); + return performCreate2Data; +} + +export async function proposeAndWaitForSafeTx( + hre: HardhatRuntimeEnvironment, + opts: DeployProxyOptions, + to: string, + callData: string, +) { + const metaTxData: MetaTransactionData = { + to, + data: callData, + value: '0', + operation: OperationType.Call, + }; + + const chainId = hre.network.config.chainId ?? (await getChainId(hre.network.provider)); + const apiKit = new SafeApiKit({ + chainId: toBigInt(chainId), + txServiceUrl: opts.txServiceUrl ?? 'https://safe-transaction-mainnet.safe.global/api', + }); + + const protocolKitOwner1 = await Safe.init({ + provider: hre.network.provider, + safeAddress: opts.safeAddress ?? '', + contractNetworks: { + [chainId]: { + // 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', + }, + }, + }); + + // Sign and send the transaction + // Create a Safe transaction with the provided parameters + 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: opts.safeAddress ?? '', + safeTransactionData: safeTransaction.data, + safeTxHash, + senderAddress: senderSignature.signer, + senderSignature: senderSignature.data, + }); + + // wait until tx is signed & executed + 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(['event ContractCreation(address newContract)']); + 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') { + return [parsedLog?.args.newContract, tx, receipt]; + } + } catch (error) { + console.error('Error parsing log:', error); + } + } + return ['', tx, receipt]; +} diff --git a/packages/plugin-hardhat/src/safeglobal/upgrade.ts b/packages/plugin-hardhat/src/safeglobal/upgrade.ts new file mode 100644 index 000000000..fb45919b1 --- /dev/null +++ b/packages/plugin-hardhat/src/safeglobal/upgrade.ts @@ -0,0 +1,133 @@ +import { Interface, TransactionResponse } from 'ethers'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +import { getChainId } from '@openzeppelin/upgrades-core'; +import { proposeAndWaitForSafeTx } 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, + 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); +} + +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); +} + +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); +} + +export 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-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; diff --git a/packages/plugin-hardhat/src/upgrade-proxy.ts b/packages/plugin-hardhat/src/upgrade-proxy.ts index 0bf2acde9..d218beb42 100644 --- a/packages/plugin-hardhat/src/upgrade-proxy.ts +++ b/packages/plugin-hardhat/src/upgrade-proxy.ts @@ -18,6 +18,14 @@ import { attachProxyAdminV4, attachProxyAdminV5, } from './utils/attach-abi'; +import { + safeGlobalAdminUpgradeAndCallV4, + safeGlobalAdminUpgradeAndCallV5, + safeGlobalAdminUpgradeV4, + safeGlobalUpgradeToAndCallV4, + safeGlobalUpgradeToAndCallV5, + safeGlobalUpgradeToV4, +} from './safeglobal/upgrade'; export type UpgradeFunction = ( proxy: ContractAddressOrInstance, @@ -62,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); } @@ -73,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); @@ -83,6 +100,10 @@ export function makeUpgradeProxy( const upgradeInterfaceVersion = await getUpgradeInterfaceVersion(provider, adminAddress, log); switch (upgradeInterfaceVersion) { case '5.0.0': { + if (opts.useSafeGlobalDeploy) { + return (nextImpl, call) => + 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); } @@ -94,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 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..91b977f4f 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 SafeGlobal 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"