Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deploying & Upgrading smart contract using multi-sig [Without using defender] #1052

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
3 changes: 3 additions & 0 deletions packages/plugin-hardhat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 19 additions & 11 deletions packages/plugin-hardhat/src/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('✔') + ' ';

Expand All @@ -15,13 +16,13 @@ export type ChangeAdminFunction = (
proxyAddress: string,
newAdmin: string,
signer?: Signer,
opts?: EthersDeployOptions,
opts?: EthersDeployOptions & SafeGlobalDeployOptions,
) => Promise<void>;
export type TransferProxyAdminOwnershipFunction = (
proxyAddress: string,
newOwner: string,
signer?: Signer,
opts?: TransferProxyAdminOwnershipOptions & EthersDeployOptions,
opts?: TransferProxyAdminOwnershipOptions & EthersDeployOptions & SafeGlobalDeployOptions,
) => Promise<void>;
export type GetInstanceFunction = (signer?: Signer) => Promise<Contract>;

Expand All @@ -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);
}
};
}

Expand All @@ -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;
Expand Down
39 changes: 39 additions & 0 deletions packages/plugin-hardhat/src/safeglobal/admin.ts
Original file line number Diff line number Diff line change
@@ -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<TransactionResponse> {
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<TransactionResponse> {
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);
}
139 changes: 139 additions & 0 deletions packages/plugin-hardhat/src/safeglobal/deploy.ts
Original file line number Diff line number Diff line change
@@ -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<Required<Deployment & DeployTransaction> & 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<string> {
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<string>(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];
}
133 changes: 133 additions & 0 deletions packages/plugin-hardhat/src/safeglobal/upgrade.ts
Original file line number Diff line number Diff line change
@@ -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<TransactionResponse> {
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<TransactionResponse> {
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<TransactionResponse> {
return safeGlobalUpgradeToAndCallV5(hre, opts, proxyAddress, nextImpl, call);
}

export async function safeGlobalAdminUpgradeAndCallV5(
hre: HardhatRuntimeEnvironment,
opts: UpgradeProxyOptions,
adminAddress: string,
proxyAddress: string,
nextImpl: string,
call: string,
): Promise<TransactionResponse> {
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<TransactionResponse> {
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<TransactionResponse> {
return safeGlobalAdminUpgradeAndCallV5(hre, opts, adminAddress, proxyAddress, nextImpl, call);
}

export async function safeGlobalBeaconUpgradeTo(
hre: HardhatRuntimeEnvironment,
opts: UpgradeProxyOptions,
beaconAddress: string,
nextImpl: string,
): Promise<TransactionResponse> {
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<TransactionResponse> {
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,
);
}
Loading