UNPKG

hardhat-deploy

Version:

Hardhat Plugin For Replicable Deployments And Tests

1,564 lines (1,454 loc) 114 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import {Signer} from '@ethersproject/abstract-signer'; import { Web3Provider, TransactionResponse, TransactionRequest, } from '@ethersproject/providers'; import {getAddress} from '@ethersproject/address'; import { Contract, ContractFactory, PayableOverrides, } from '@ethersproject/contracts'; import * as zk from 'zksync-ethers'; import {AddressZero} from '@ethersproject/constants'; import {BigNumber} from '@ethersproject/bignumber'; import {Wallet} from '@ethersproject/wallet'; import {keccak256 as solidityKeccak256} from '@ethersproject/solidity'; import {zeroPad, hexlify, hexConcat} from '@ethersproject/bytes'; import {Interface, FunctionFragment} from '@ethersproject/abi'; import { Deployment, DeployResult, DeploymentsExtension, DeployOptions, TxOptions, CallOptions, SimpleTx, Receipt, Address, DiamondOptions, Create2DeployOptions, FacetCut, DeploymentSubmission, ExtendedArtifact, FacetCutAction, Facet, ArtifactData, ABI, } from '../types'; import {PartialExtension} from './internal/types'; import {UnknownSignerError} from './errors'; import {filterABI, mergeABIs, recode} from './utils'; import fs from 'fs-extra'; import OpenZeppelinTransparentProxy from '../extendedArtifacts/TransparentUpgradeableProxy.json'; import OptimizedTransparentUpgradeableProxy from '../extendedArtifacts/OptimizedTransparentUpgradeableProxy.json'; import DefaultProxyAdmin from '../extendedArtifacts/ProxyAdmin.json'; import eip173Proxy from '../extendedArtifacts/EIP173Proxy.json'; import eip173ProxyWithReceive from '../extendedArtifacts/EIP173ProxyWithReceive.json'; import erc1967Proxy from '../extendedArtifacts/ERC1967Proxy.json'; import diamondBase from '../extendedArtifacts/Diamond.json'; import oldDiamonBase from './old_diamondbase.json'; import diamondERC165Init from '../extendedArtifacts/DiamondERC165Init.json'; import diamondCutFacet from '../extendedArtifacts/DiamondCutFacet.json'; import diamondLoupeFacet from '../extendedArtifacts/DiamondLoupeFacet.json'; import ownershipFacet from '../extendedArtifacts/OwnershipFacet.json'; import {Artifact, EthereumProvider, Network} from 'hardhat/types'; import {DeploymentsManager} from './DeploymentsManager'; import enquirer from 'enquirer'; import { parse as parseTransaction, Transaction, } from '@ethersproject/transactions'; import {getDerivationPath} from './hdpath'; import {bnReplacer} from './internal/utils'; import {DeploymentFactory} from './DeploymentFactory'; let LedgerSigner: any; // TODO type let ethersprojectHardwareWalletsModule: any | undefined; let andrestEthersLedgerModule: any | undefined; let TrezorSigner: any; // TODO type let hardwareSigner: any; // TODO type async function handleSpecificErrors<T>(p: Promise<T>): Promise<T> { let result: T; try { result = await p; } catch (e) { if ( typeof (e as any).message === 'string' && (e as any).message.indexOf('already known') !== -1 ) { console.log( ` Exact same transaction already in the pool, node reject duplicates. You'll need to wait the tx resolve, or increase the gas price via --gasprice (this will use old tx type) ` ); throw new Error( 'Exact same transaction already in the pool, node reject duplicates' ); // console.log( // `\nExact same transaction already in the pool, node reject duplicates, waiting for it instead...\n` // ); // const signedTx = await ethersSigner.signTransaction(unsignedTx); // const decoded = parseTransaction(signedTx); // if (!decoded.hash) { // throw new Error( // 'tx with same hash already in the pool, failed to decode to get the hash' // ); // } // const txHash = decoded.hash; // tx = Object.assign(decoded as TransactionResponse, { // wait: (confirmations: number) => // provider.waitForTransaction(txHash, confirmations), // confirmations: 0, // }); } else { console.error((e as any).message, JSON.stringify(e, bnReplacer), e); throw e; } } return result; } function fixProvider(providerGiven: any): any { // allow it to be used by ethers without any change if (providerGiven.sendAsync === undefined) { providerGiven.sendAsync = ( req: { id: number; jsonrpc: string; method: string; params: any[]; }, callback: (error: any, result: any) => void ) => { providerGiven .send(req.method, req.params) .then((result: any) => callback(null, {result, id: req.id, jsonrpc: req.jsonrpc}) ) .catch((error: any) => callback(error, null)); }; } return providerGiven; } function findAll(toFind: string[], array: string[]): boolean { for (const f of toFind) { if (array.indexOf(f) === -1) { return false; } } return true; } function linkRawLibrary( bytecode: string, libraryName: string, libraryAddress: string ): string { const address = libraryAddress.replace('0x', ''); let encodedLibraryName; if (libraryName.startsWith('$') && libraryName.endsWith('$')) { encodedLibraryName = libraryName.slice(1, libraryName.length - 1); } else { encodedLibraryName = solidityKeccak256(['string'], [libraryName]).slice( 2, 36 ); } const pattern = new RegExp(`_+\\$${encodedLibraryName}\\$_+`, 'g'); if (!pattern.exec(bytecode)) { throw new Error( `Can't link '${libraryName}' (${encodedLibraryName}) in \n----\n ${bytecode}\n----\n` ); } return bytecode.replace(pattern, address); } function linkRawLibraries( bytecode: string, libraries: {[libraryName: string]: Address} ): string { for (const libName of Object.keys(libraries)) { const libAddress = libraries[libName]; bytecode = linkRawLibrary(bytecode, libName, libAddress); } return bytecode; } function linkLibraries( artifact: { bytecode: string; linkReferences?: { [libraryFileName: string]: { [libraryName: string]: Array<{length: number; start: number}>; }; }; }, libraries?: {[libraryName: string]: Address} ) { let bytecode = artifact.bytecode; if (libraries) { if (artifact.linkReferences) { // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const [fileName, fileReferences] of Object.entries( artifact.linkReferences )) { for (const [libName, fixups] of Object.entries(fileReferences)) { const addr = libraries[libName]; if (addr === undefined) { continue; } for (const fixup of fixups) { bytecode = bytecode.substr(0, 2 + fixup.start * 2) + addr.substr(2) + bytecode.substr(2 + (fixup.start + fixup.length) * 2); } } } } else { bytecode = linkRawLibraries(bytecode, libraries); } } // TODO return libraries object with path name <filepath.sol>:<name> for names return bytecode; } export function addHelpers( deploymentManager: DeploymentsManager, partialExtension: PartialExtension, network: any, // TODO work out right config type getArtifact: (name: string) => Promise<Artifact>, saveDeployment: ( name: string, deployment: DeploymentSubmission, artifactName?: string ) => Promise<void>, willSaveToDisk: () => boolean, onPendingTx: ( txResponse: TransactionResponse, name?: string, data?: any ) => Promise<TransactionResponse>, getGasPrice: () => Promise<{ gasPrice: BigNumber | undefined; maxFeePerGas: BigNumber | undefined; maxPriorityFeePerGas: BigNumber | undefined; }>, log: (...args: any[]) => void, print: (msg: string) => void ): { extension: DeploymentsExtension; utils: { dealWithPendingTransactions: ( pendingTxs: { [txHash: string]: { name: string; deployment?: any; rawTx: string; decoded: { from: string; gasPrice?: string; maxFeePerGas?: string; maxPriorityFeePerGas?: string; gasLimit: string; to: string; value: string; nonce: number; data: string; r: string; s: string; v: number; // creates: tx.creates, // TODO test chainId: number; }; }; }, pendingTxPath: string, globalGasPrice: string | undefined ) => Promise<void>; }; } { let provider: Web3Provider | zk.Web3Provider; const availableAccounts: {[name: string]: boolean} = {}; async function init(): Promise<Web3Provider | zk.Web3Provider> { if (!provider) { await deploymentManager.setupAccounts(); if (network.zksync) { provider = new zk.Web3Provider(fixProvider(network.provider)); } else { provider = new Web3Provider(fixProvider(network.provider)); } try { const accounts = await provider.send('eth_accounts', []); for (const account of accounts) { availableAccounts[account.toLowerCase()] = true; } for (const address of deploymentManager.impersonatedAccounts) { availableAccounts[address.toLowerCase()] = true; } } catch (e) {} } return provider; } function cleanupOverrides<T extends PayableOverrides>( txRequestOrOverrides: T ): T { if (txRequestOrOverrides.maxFeePerGas === undefined) { delete txRequestOrOverrides.maxFeePerGas; } if (txRequestOrOverrides.maxPriorityFeePerGas === undefined) { delete txRequestOrOverrides.maxPriorityFeePerGas; } if (txRequestOrOverrides.gasPrice === undefined) { delete txRequestOrOverrides.gasPrice; } if (txRequestOrOverrides.value === undefined) { delete txRequestOrOverrides.value; } return txRequestOrOverrides; } async function setupGasPrice( txRequestOrOverrides: TransactionRequest | PayableOverrides ) { const gasPriceSetup = await getGasPrice(); if (!txRequestOrOverrides.gasPrice) { txRequestOrOverrides.gasPrice = gasPriceSetup.gasPrice; } if (!txRequestOrOverrides.maxFeePerGas) { txRequestOrOverrides.maxFeePerGas = gasPriceSetup.maxFeePerGas; } if (!txRequestOrOverrides.maxPriorityFeePerGas) { txRequestOrOverrides.maxPriorityFeePerGas = gasPriceSetup.maxPriorityFeePerGas; } cleanupOverrides(txRequestOrOverrides); } async function setupNonce( from: string, txRequestOrOverrides: TransactionRequest | PayableOverrides ) { if ( txRequestOrOverrides.nonce === 'pending' || txRequestOrOverrides.nonce === 'latest' ) { txRequestOrOverrides.nonce = await provider.getTransactionCount( from, txRequestOrOverrides.nonce ); } else if (!txRequestOrOverrides.nonce) { txRequestOrOverrides.nonce = await provider.getTransactionCount( from, 'latest' ); } } async function overrideGasLimit( txRequestOrOverrides: TransactionRequest | PayableOverrides, options: { estimatedGasLimit?: number | BigNumber | string; estimateGasExtra?: number | BigNumber | string; }, estimate: ( txRequestOrOverrides: TransactionRequest | PayableOverrides ) => Promise<BigNumber> ) { const estimatedGasLimit = options.estimatedGasLimit ? BigNumber.from(options.estimatedGasLimit).toNumber() : undefined; const estimateGasExtra = options.estimateGasExtra ? BigNumber.from(options.estimateGasExtra).toNumber() : undefined; if (!txRequestOrOverrides.gasLimit) { txRequestOrOverrides.gasLimit = estimatedGasLimit; txRequestOrOverrides.gasLimit = ( await estimate(txRequestOrOverrides) ).toNumber(); if (estimateGasExtra) { txRequestOrOverrides.gasLimit = txRequestOrOverrides.gasLimit + estimateGasExtra; if (estimatedGasLimit) { txRequestOrOverrides.gasLimit = Math.min( txRequestOrOverrides.gasLimit, estimatedGasLimit ); } } } } async function ensureCreate2DeployerReady(options: { from: string; log?: boolean; waitConfirmations?: number; gasPrice?: string | BigNumber; maxFeePerGas?: string | BigNumber; maxPriorityFeePerGas?: string | BigNumber; }): Promise<string> { const { address: from, ethersSigner, hardwareWallet, unknown, } = await getFrom(options.from); const create2DeployerAddress = await deploymentManager.getDeterministicDeploymentFactoryAddress(); const code = await provider.getCode(create2DeployerAddress); if (code === '0x') { const senderAddress = await deploymentManager.getDeterministicDeploymentFactoryDeployer(); // TODO: calculate required funds const txRequest = { to: senderAddress, value: ( await deploymentManager.getDeterministicDeploymentFactoryFunding() ).toHexString(), gasPrice: options.gasPrice, maxFeePerGas: options.maxFeePerGas, maxPriorityFeePerGas: options.maxPriorityFeePerGas, }; await setupGasPrice(txRequest); await setupNonce(from, txRequest); if (unknown) { throw new UnknownSignerError({ from, ...txRequest, }); } if (options.log || hardwareWallet) { print( `sending eth to create2 contract deployer address (${senderAddress})` ); if (hardwareWallet) { print(` (please confirm on your ${hardwareWallet})`); } } let ethTx = (await handleSpecificErrors( ethersSigner.sendTransaction(txRequest) )) as TransactionResponse; if (options.log || hardwareWallet) { log(` (tx: ${ethTx.hash})...`); } ethTx = await onPendingTx(ethTx); await ethTx.wait(options.waitConfirmations); if (options.log || hardwareWallet) { print( `deploying create2 deployer contract (at ${create2DeployerAddress}) using deterministic deployment (https://github.com/Arachnid/deterministic-deployment-proxy)` ); if (hardwareWallet) { print(` (please confirm on your ${hardwareWallet})`); } } const deployTx = await provider.sendTransaction( await deploymentManager.getDeterministicDeploymentFactoryDeploymentTx() ); if (options.log || hardwareWallet) { log(` (tx: ${deployTx.hash})...`); } await deployTx.wait(options.waitConfirmations); } return create2DeployerAddress; } async function getArtifactFromOptions( name: string, options: DeployOptions ): Promise<{ artifact: Artifact; artifactName?: string; }> { let artifact: Artifact; let artifactName: string | undefined; if (options.contract) { if (typeof options.contract === 'string') { artifactName = options.contract; artifact = await getArtifact(artifactName); } else { artifact = options.contract as Artifact; // TODO better handling } } else { artifactName = name; artifact = await getArtifact(artifactName); } return {artifact, artifactName}; } async function getLinkedArtifact( name: string, options: DeployOptions ): Promise<{artifact: Artifact; artifactName: string | undefined}> { // TODO get linked artifact const {artifact, artifactName} = await getArtifactFromOptions( name, options ); const byteCode = linkLibraries(artifact, options.libraries); return {artifact: {...artifact, bytecode: byteCode}, artifactName}; } async function _deploy( name: string, options: DeployOptions ): Promise<DeployResult> { const args: any[] = options.args ? [...options.args] : []; await init(); const { address: from, ethersSigner, hardwareWallet, unknown, } = await getFrom(options.from); const {artifact: linkedArtifact, artifactName} = await getLinkedArtifact( name, options ); const overrides: PayableOverrides = { gasLimit: options.gasLimit, gasPrice: options.gasPrice, maxFeePerGas: options.maxFeePerGas, maxPriorityFeePerGas: options.maxPriorityFeePerGas, value: options.value, nonce: options.nonce, }; if (options.customData !== undefined) { overrides.customData = options.customData; } const factory = new DeploymentFactory( getArtifact, linkedArtifact, args, network, ethersSigner, overrides ); const unsignedTx = await factory.getDeployTransaction(); let create2Address; if (options.deterministicDeployment) { if (typeof unsignedTx.data === 'string') { const create2DeployerAddress = await ensureCreate2DeployerReady( options ); const create2Salt = typeof options.deterministicDeployment === 'string' ? hexlify(zeroPad(options.deterministicDeployment, 32)) : '0x0000000000000000000000000000000000000000000000000000000000000000'; create2Address = await factory.getCreate2Address( create2DeployerAddress, create2Salt ); unsignedTx.to = create2DeployerAddress; unsignedTx.data = create2Salt + unsignedTx.data.slice(2); } else { throw new Error('unsigned tx data as bytes not supported'); } } await overrideGasLimit(unsignedTx, options, (newOverrides) => ethersSigner.estimateGas(newOverrides) ); await setupGasPrice(unsignedTx); await setupNonce(from, unsignedTx); // Temporary workaround for https://github.com/ethers-io/ethers.js/issues/2078 // TODO: Remove me when LedgerSigner adds proper support for 1559 txns if (hardwareWallet === 'ledger') { unsignedTx.type = 1; } else if (hardwareWallet === 'trezor') { unsignedTx.type = 1; } if (unknown) { throw new UnknownSignerError({ from, ...JSON.parse(JSON.stringify(unsignedTx, bnReplacer)), }); } if (options.log || hardwareWallet) { print(`deploying "${name}"`); if (hardwareWallet) { print(` (please confirm on your ${hardwareWallet})`); } } let tx = (await handleSpecificErrors( ethersSigner.sendTransaction(unsignedTx) )) as TransactionResponse; if (options.log || hardwareWallet) { print(` (tx: ${tx.hash})...`); } if (options.autoMine) { try { await provider.send('evm_mine', []); } catch (e) {} } let preDeployment = { ...linkedArtifact, transactionHash: tx.hash, args, linkedData: options.linkedData, }; if (artifactName && willSaveToDisk()) { const extendedArtifact = await partialExtension.getExtendedArtifact( artifactName ); preDeployment = { ...extendedArtifact, ...preDeployment, }; } tx = await onPendingTx(tx, name, preDeployment); const receipt = await tx.wait(options.waitConfirmations); const address = factory.getDeployedAddress( receipt, options, create2Address ); const deployment = { ...preDeployment, address, receipt, transactionHash: receipt.transactionHash, libraries: options.libraries, factoryDeps: unsignedTx.customData?.factoryDeps || [], }; await saveDeployment(name, deployment); if (options.log || hardwareWallet) { print( `: deployed at ${deployment.address} with ${receipt?.gasUsed} gas\n` ); } return { ...deployment, address, newlyDeployed: true, }; } async function deterministic( name: string, options: Create2DeployOptions ): Promise<{ address: Address; implementationAddress?: Address; deploy: () => Promise<DeployResult>; }> { options = {...options}; // ensure no change await init(); const deployFunction = () => deploy(name, { ...options, deterministicDeployment: options.salt || true, }); if (options.proxy) { /* eslint-disable prefer-const */ let { viaAdminContract, proxyAdminDeployed, proxyAdminName, proxyAdminContract, owner, proxyAdmin, currentProxyAdminOwner, artifact, implementationArgs, implementationName, implementationOptions, proxyName, proxyContract, proxyArgsTemplate, mergedABI, updateMethod, updateArgs, } = await _getProxyInfo(name, options); /* eslint-enable prefer-const */ const {address: implementationAddress} = await deterministic( implementationName, {...implementationOptions, salt: options.salt} ); const implementationContract = new Contract( implementationAddress, artifact.abi ); let data = '0x'; if (updateMethod) { updateArgs = updateArgs || []; if (!implementationContract[updateMethod]) { throw new Error( `contract need to implement function ${updateMethod}` ); } const txData = await implementationContract.populateTransaction[ updateMethod ](...updateArgs); data = txData.data || '0x'; } if (viaAdminContract) { if (!proxyAdminName) { throw new Error( `no proxy admin name even though viaAdminContract is not undefined` ); } if (!proxyAdminDeployed) { const {address: proxyAdminAddress} = await deterministic( proxyAdminName, { from: options.from, autoMine: options.autoMine, estimateGasExtra: options.estimateGasExtra, estimatedGasLimit: options.estimatedGasLimit, gasPrice: options.gasPrice, maxFeePerGas: options.maxFeePerGas, maxPriorityFeePerGas: options.maxPriorityFeePerGas, log: options.log, contract: proxyAdminContract, salt: options.salt, skipIfAlreadyDeployed: true, args: [owner], waitConfirmations: options.waitConfirmations, } ); proxyAdmin = proxyAdminAddress; } else { proxyAdmin = proxyAdminDeployed.address; } } const proxyOptions = {...options}; // ensure no change delete proxyOptions.proxy; delete proxyOptions.libraries; proxyOptions.contract = proxyContract; proxyOptions.args = replaceTemplateArgs(proxyArgsTemplate, { implementationAddress, proxyAdmin, data, }); const {address: proxyAddress} = await deterministic(proxyName, { ...proxyOptions, salt: options.salt, }); return { address: proxyAddress, implementationAddress, deploy: deployFunction, }; } else { const args: any[] = options.args ? [...options.args] : []; const { ethersSigner, unknown, address: from, } = await getFrom(options.from); const {artifact: linkedArtifact, artifactName} = await getLinkedArtifact( name, options ); const factory = new DeploymentFactory( getArtifact, linkedArtifact, args, network, ethersSigner ); if (unknown) { throw new UnknownSignerError({ from, ...JSON.parse( JSON.stringify(await factory.getDeployTransaction(), bnReplacer) ), }); } return { address: await factory.getCreate2Address( await deploymentManager.getDeterministicDeploymentFactoryAddress(), options.salt ? hexlify(zeroPad(options.salt, 32)) : '0x0000000000000000000000000000000000000000000000000000000000000000' ), deploy: () => deploy(name, { ...options, deterministicDeployment: options.salt || true, }), }; } } function getDeployment(name: string): Promise<Deployment> { return partialExtension.get(name); } function getDeploymentOrNUll(name: string): Promise<Deployment | null> { return partialExtension.getOrNull(name); } async function fetchIfDifferent( name: string, options: DeployOptions ): Promise<{differences: boolean; address?: string}> { options = {...options}; // ensure no change const args = options.args ? [...options.args] : []; await init(); const {ethersSigner} = await getFrom(options.from); const {artifact: linkedArtifact} = await getLinkedArtifact(name, options); const factory = new DeploymentFactory( getArtifact, linkedArtifact, args, network, ethersSigner ); if (options.deterministicDeployment) { const create2Salt = typeof options.deterministicDeployment === 'string' ? hexlify(zeroPad(options.deterministicDeployment, 32)) : '0x0000000000000000000000000000000000000000000000000000000000000000'; const create2DeployerAddress = await deploymentManager.getDeterministicDeploymentFactoryAddress(); const create2Address = await factory.getCreate2Address( create2DeployerAddress, create2Salt ); const code = await provider.getCode(create2Address); if (code === '0x') { return {differences: true, address: undefined}; } else { return {differences: false, address: create2Address}; } } const deployment = await partialExtension.getOrNull(name); if (deployment) { if (options.skipIfAlreadyDeployed) { return {differences: false, address: undefined}; // TODO check receipt, see below } // TODO transactionReceipt + check for status let transactionDetailsAvailable = false; let transaction; if (deployment.receipt) { transactionDetailsAvailable = !!deployment.receipt.transactionHash; if (transactionDetailsAvailable) { transaction = await provider.getTransaction( deployment.receipt.transactionHash ); } } else if (deployment.transactionHash) { transactionDetailsAvailable = true; transaction = await provider.getTransaction(deployment.transactionHash); } if (transaction) { const differences = await factory.compareDeploymentTransaction( transaction, deployment ); return {differences, address: deployment.address}; } else { if (transactionDetailsAvailable) { throw new Error( `cannot get the transaction for ${name}'s previous deployment, please check your node synced status.` ); } else { console.error( `no transaction details found for ${name}'s previous deployment, if the deployment is t be discarded, please delete the file` ); return {differences: false, address: deployment.address}; } } } return {differences: true, address: undefined}; } async function _deployOne( name: string, options: DeployOptions, failsOnExistingDeterminisitc?: boolean ): Promise<DeployResult> { const argsArray = options.args ? [...options.args] : []; options = {...options, args: argsArray}; let result: DeployResult; const diffResult = await fetchIfDifferent(name, options); if (diffResult.differences) { result = await _deploy(name, options); } else { if (failsOnExistingDeterminisitc && options.deterministicDeployment) { throw new Error( `already deployed on same deterministic address: ${diffResult.address}` ); } const deployment = await getDeploymentOrNUll(name); if (deployment) { if ( options.deterministicDeployment && diffResult.address && diffResult.address.toLowerCase() !== deployment.address.toLowerCase() ) { const {artifact: linkedArtifact, artifactName} = await getLinkedArtifact(name, options); // receipt missing const newDeployment = { ...linkedArtifact, address: diffResult.address, linkedData: options.linkedData, libraries: options.libraries, args: argsArray, }; await saveDeployment(name, newDeployment, artifactName); result = { ...newDeployment, newlyDeployed: false, }; } else { result = deployment as DeployResult; result.newlyDeployed = false; } } else { if (!diffResult.address) { throw new Error( 'no differences found but no address, this should be impossible' ); } const {artifact: linkedArtifact, artifactName} = await getLinkedArtifact(name, options); // receipt missing const newDeployment = { ...linkedArtifact, address: diffResult.address, linkedData: options.linkedData, libraries: options.libraries, args: argsArray, }; await saveDeployment(name, newDeployment, artifactName); result = { ...newDeployment, newlyDeployed: false, }; } if (options.log) { log(`reusing "${name}" at ${result.address}`); } } return result; } function _checkUpgradeIndex( oldDeployment: Deployment | null, upgradeIndex?: number ): DeployResult | undefined { if (typeof upgradeIndex === 'undefined') { return; } if (upgradeIndex === 0) { if (oldDeployment) { return {...oldDeployment, newlyDeployed: false}; } } else if (upgradeIndex === 1) { if (!oldDeployment) { throw new Error( 'upgradeIndex === 1 : expects Deployments to already exists' ); } if ( (oldDeployment.history && oldDeployment.history.length > 0) || (oldDeployment.numDeployments && oldDeployment.numDeployments > 1) ) { return {...oldDeployment, newlyDeployed: false}; } } else { if (!oldDeployment) { throw new Error( `upgradeIndex === ${upgradeIndex} : expects Deployments to already exists` ); } if (!oldDeployment.history) { if (oldDeployment.numDeployments && oldDeployment.numDeployments > 1) { if (oldDeployment.numDeployments > upgradeIndex) { return {...oldDeployment, newlyDeployed: false}; } else if (oldDeployment.numDeployments < upgradeIndex) { throw new Error( `upgradeIndex === ${upgradeIndex} : expects Deployments numDeployments to be at least ${upgradeIndex}` ); } } else { throw new Error( `upgradeIndex > 1 : expects Deployments history to exists, or numDeployments to be greater than 1` ); } } else if (oldDeployment.history.length > upgradeIndex - 1) { return {...oldDeployment, newlyDeployed: false}; } else if (oldDeployment.history.length < upgradeIndex - 1) { throw new Error( `upgradeIndex === ${upgradeIndex} : expects Deployments history length to be at least ${ upgradeIndex - 1 }` ); } } } async function _getProxyInfo( name: string, options: DeployOptions ): Promise<{ viaAdminContract: | string | {name: string; artifact?: string | ArtifactData} | undefined; proxyAdminName: string | undefined; proxyAdminDeployed: Deployment | undefined; proxyAdmin: string; proxyAdminContract: ExtendedArtifact | undefined; owner: string; currentProxyAdminOwner: string | undefined; artifact: ExtendedArtifact; implementationArgs: any[]; implementationName: string; implementationOptions: DeployOptions; mergedABI: ABI; proxyName: string; proxyContract: ExtendedArtifact; proxyArgsTemplate: any[]; oldDeployment: Deployment | null; updateMethod: string | undefined; updateArgs: any[]; upgradeIndex: number | undefined; checkProxyAdmin: boolean; upgradeMethod: string | undefined; upgradeArgsTemplate: any[]; }> { const oldDeployment = await getDeploymentOrNUll(name); let contractName = options.contract; let implementationName = name + '_Implementation'; let updateMethod: string | undefined; let updateArgs: any[] | undefined; let upgradeIndex; let proxyContract: ExtendedArtifact = eip173Proxy; let checkABIConflict = true; let checkProxyAdmin = true; let viaAdminContract: | string | {name: string; artifact?: string | ArtifactData} | undefined; let proxyArgsTemplate = ['{implementation}', '{admin}', '{data}']; let upgradeMethod: string | undefined; let upgradeArgsTemplate: string[] = []; if (typeof options.proxy === 'object') { if (options.proxy.proxyArgs) { proxyArgsTemplate = options.proxy.proxyArgs; } upgradeIndex = options.proxy.upgradeIndex; if (options.proxy.implementationName) { implementationName = options.proxy.implementationName; if (implementationName === name) { throw new Error( `"implementationName" cannot be equal to the deployment's name (${name}) as this is used for the proxy itself.` ); } if (!contractName) { contractName = implementationName; } } if ('methodName' in options.proxy) { updateMethod = options.proxy.methodName; if ('execute' in options.proxy) { throw new Error( `cannot have both "methodName" and "execute" options for proxy` ); } } else if ('execute' in options.proxy && options.proxy.execute) { if ('methodName' in options.proxy.execute) { updateMethod = options.proxy.execute.methodName; updateArgs = options.proxy.execute.args; if ( 'init' in options.proxy.execute || 'onUpgrade' in options.proxy.execute ) { throw new Error( `cannot have both "methodName" and ("onUpgrade" or "init") options for proxy.execute` ); } } else if ( ('init' in options.proxy.execute && options.proxy.execute.init) || ('onUpgrade' in options.proxy.execute && options.proxy.execute.onUpgrade) ) { if (oldDeployment) { updateMethod = options.proxy.execute.onUpgrade?.methodName; updateArgs = options.proxy.execute.onUpgrade?.args; } else { updateMethod = options.proxy.execute.init.methodName; updateArgs = options.proxy.execute.init.args; } } } checkABIConflict = options.proxy.checkABIConflict ?? checkABIConflict; checkProxyAdmin = options.proxy.checkProxyAdmin ?? checkProxyAdmin; if (options.proxy.proxyContract) { if (typeof options.proxy.proxyContract === 'string') { try { proxyContract = await partialExtension.getExtendedArtifact( options.proxy.proxyContract ); } catch (e) {} if (!proxyContract || proxyContract === eip173Proxy) { if (options.proxy.proxyContract === 'EIP173ProxyWithReceive') { proxyContract = eip173ProxyWithReceive; } else if (options.proxy.proxyContract === 'EIP173Proxy') { proxyContract = eip173Proxy; } else if ( options.proxy.proxyContract === 'OpenZeppelinTransparentProxy' ) { checkABIConflict = false; proxyContract = OpenZeppelinTransparentProxy; viaAdminContract = 'DefaultProxyAdmin'; } else if ( options.proxy.proxyContract === 'OptimizedTransparentProxy' ) { checkABIConflict = false; proxyContract = OptimizedTransparentUpgradeableProxy; viaAdminContract = 'DefaultProxyAdmin'; // } else if (options.proxy.proxyContract === 'UUPS') { // checkABIConflict = true; // proxyContract = UUPSProxy; } else if (options.proxy.proxyContract === 'UUPS') { checkABIConflict = false; checkProxyAdmin = false; proxyContract = erc1967Proxy; proxyArgsTemplate = ['{implementation}', '{data}']; } else { throw new Error( `no contract found for ${options.proxy.proxyContract}` ); } } } } if (options.proxy.viaAdminContract) { viaAdminContract = options.proxy.viaAdminContract; } if (options.proxy.upgradeFunction) { upgradeMethod = options.proxy.upgradeFunction.methodName; upgradeArgsTemplate = options.proxy.upgradeFunction.upgradeArgs; } } else if (typeof options.proxy === 'string') { updateMethod = options.proxy; } const proxyName = name + '_Proxy'; const {address: owner} = await getProxyOwner(options); const implementationArgs = options.args ? [...options.args] : []; // --- Implementation Deployment --- const implementationOptions = { contract: contractName || name, from: options.from, autoMine: options.autoMine, estimateGasExtra: options.estimateGasExtra, estimatedGasLimit: options.estimatedGasLimit, gasPrice: options.gasPrice, maxFeePerGas: options.maxFeePerGas, maxPriorityFeePerGas: options.maxPriorityFeePerGas, log: options.log, deterministicDeployment: options.deterministicDeployment, libraries: options.libraries, linkedData: options.linkedData, args: implementationArgs, skipIfAlreadyDeployed: options.skipIfAlreadyDeployed, waitConfirmations: options.waitConfirmations, }; const {artifact} = await getArtifactFromOptions( name, implementationOptions ); const proxyContractConstructor = proxyContract.abi.find( (v) => v.type === 'constructor' ); // ensure no clash const mergedABI = mergeABIs([proxyContract.abi, artifact.abi], { check: checkABIConflict, skipSupportsInterface: true, // TODO options for custom proxy ? }).filter((v) => v.type !== 'constructor'); mergedABI.push(proxyContractConstructor); // use proxy constructor abi const constructor = artifact.abi.find( (fragment: {type: string; inputs: any[]}) => fragment.type === 'constructor' ); if ( (!constructor && implementationArgs.length > 0) || (constructor && constructor.inputs.length !== implementationArgs.length) ) { throw new Error( `The number of arguments passed to not match the number of argument in the implementation constructor. Please specify the correct number of arguments as part of the deploy options: "args"` ); } if (updateMethod) { const updateMethodFound: { type: string; inputs: any[]; name: string; } = artifact.abi.find( (fragment: {type: string; inputs: any[]; name: string}) => fragment.type === 'function' && fragment.name === updateMethod ); if (!updateMethodFound) { throw new Error(`contract need to implement function ${updateMethod}`); } if (!updateArgs) { if (implementationArgs.length === updateMethodFound.inputs.length) { updateArgs = implementationArgs; } else { throw new Error( ` If only the methodName (and no args) is specified for proxy deployment, the arguments used for the implementation contract will be reused for the update method. This allow your contract to both be deployed directly and deployed via proxy. Currently your contract implementation's constructor do not have the same number of arguments as the update method. You can either changes the contract or use the "execute" options and specify different arguments for the update method. Note that in this case, the contract deployment will not behave the same if deployed without proxy. ` ); } } } // this avoid typescript error, but should not be necessary at runtime if (!updateArgs) { updateArgs = implementationArgs; } let proxyAdminName: string | undefined; const proxyAdmin = owner; let currentProxyAdminOwner: string | undefined; let proxyAdminDeployed: Deployment | undefined; let proxyAdminContract: ExtendedArtifact | undefined; if (viaAdminContract) { let proxyAdminArtifactNameOrContract: string | ArtifactData | undefined; if (typeof viaAdminContract === 'string') { proxyAdminName = viaAdminContract; proxyAdminArtifactNameOrContract = viaAdminContract; } else { proxyAdminName = viaAdminContract.name; if (!viaAdminContract.artifact) { proxyAdminDeployed = await partialExtension.get(proxyAdminName); } proxyAdminArtifactNameOrContract = viaAdminContract.artifact; } if (typeof proxyAdminArtifactNameOrContract === 'string') { try { proxyAdminContract = await partialExtension.getExtendedArtifact( proxyAdminArtifactNameOrContract ); } catch (e) {} if (!proxyAdminContract) { if (viaAdminContract === 'DefaultProxyAdmin') { proxyAdminContract = DefaultProxyAdmin; } else { throw new Error( `no contract found for ${proxyAdminArtifactNameOrContract}` ); } } } else { proxyAdminContract = proxyAdminArtifactNameOrContract; } } // Set upgrade function if not defined by the user, based on other options if (!upgradeMethod) { if (viaAdminContract) { if (updateMethod) { upgradeMethod = 'upgradeAndCall'; upgradeArgsTemplate = ['{proxy}', '{implementation}', '{data}']; } else { upgradeMethod = 'upgrade'; upgradeArgsTemplate = ['{proxy}', '{implementation}']; } } else if (updateMethod) { upgradeMethod = 'upgradeToAndCall'; upgradeArgsTemplate = ['{implementation}', '{data}']; } else { upgradeMethod = 'upgradeTo'; upgradeArgsTemplate = ['{implementation}']; } } return { proxyName, proxyContract, proxyArgsTemplate, mergedABI, viaAdminContract, proxyAdminDeployed, proxyAdminName, proxyAdminContract, owner, proxyAdmin, currentProxyAdminOwner, artifact, implementationArgs, implementationName, implementationOptions, oldDeployment, updateMethod, updateArgs, upgradeIndex, checkProxyAdmin, upgradeMethod, upgradeArgsTemplate, }; } async function _deployViaProxy( name: string, options: DeployOptions ): Promise<DeployResult> { /* eslint-disable prefer-const */ let { oldDeployment, updateMethod, updateArgs, upgradeIndex, viaAdminContract, proxyAdminDeployed, proxyAdminName, proxyAdminContract, owner, proxyAdmin, currentProxyAdminOwner, implementationName, implementationOptions, proxyName, proxyContract, proxyArgsTemplate, mergedABI, checkProxyAdmin, upgradeMethod, upgradeArgsTemplate, } = await _getProxyInfo(name, options); /* eslint-enable prefer-const */ const deployResult = _checkUpgradeIndex(oldDeployment, upgradeIndex); if (deployResult) { return deployResult; } if (viaAdminContract) { if (!proxyAdminName) { throw new Error( `no proxy admin name even though viaAdminContract is not undefined` ); } if (!proxyAdminDeployed) { proxyAdminDeployed = await _deployOne(proxyAdminName, { from: options.from, autoMine: options.autoMine, estimateGasExtra: options.estimateGasExtra, estimatedGasLimit: options.estimatedGasLimit, gasPrice: options.gasPrice, maxFeePerGas: options.maxFeePerGas, maxPriorityFeePerGas: options.maxPriorityFeePerGas, log: options.log, contract: proxyAdminContract, deterministicDeployment: options.deterministicDeployment, skipIfAlreadyDeployed: true, args: [owner], waitConfirmations: options.waitConfirmations, }); } proxyAdmin = proxyAdminDeployed.address; currentProxyAdminOwner = (await read(proxyAdminName, 'owner')) as string; if (currentProxyAdminOwner.toLowerCase() !== owner.toLowerCase()) { throw new Error( `To change owner/admin, you need to call transferOwnership on ${proxyAdminName}` ); } if (currentProxyAdminOwner === AddressZero) { throw new Error( `The Proxy Admin (${proxyAdminName}) belongs to no-one. The Proxy cannot be upgraded anymore` ); } } const implementation = await _deployOne( implementationName, implementationOptions ); if (!oldDeployment || implementation.newlyDeployed) { // console.log(`implementation deployed at ${implementation.address} for ${implementation.receipt.gasUsed}`); const implementationContract = new Contract( implementation.address, implementation.abi ); let data = '0x'; if (updateMethod) { if (!implementationContract[updateMethod]) { throw new Error( `contract need to implement function ${updateMethod}` ); } const txData = await implementationContract.populateTransaction[ updateMethod ](...updateArgs); data = txData.data || '0x'; } let proxy = await getDeploymentOrNUll(proxyName); if (!proxy) { const proxyOptions = {...options}; // ensure no change delete proxyOptions.proxy; delete proxyOptions.libraries; proxyOptions.contract = proxyContract; proxyOptions.args = replaceTemplateArgs(proxyArgsTemplate, { implementationAddress: implementation.address, proxyAdmin, data, }); proxy = await _deployOne(proxyName, proxyOptions, true); // console.log(`proxy deployed at ${proxy.address} for ${proxy.receipt.gasUsed}`); } else { let from = options.from; let ownerStorage: string; // Use EIP173 defined owner function if present const deployedProxy = new Contract(proxy.address, proxy.abi, provider); if (deployedProxy.functions['owner']) { ownerStorage = await deployedProxy.owner(); } else { ownerStorage = await provider.getStorageAt( proxy.address, '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103' ); } const currentOwner = getAddress(`0x${ownerStorage.substr(-40)}`); if (currentOwner === AddressZero) { if (checkProxyAdmin) { throw new Error( 'The Proxy belongs to no-one. It cannot be upgraded anymore' ); } } else if (currentOwner.toLowerCase() !== proxyAdmin.toLowerCase()) { throw new Error( `To change owner/admin, you need to call the proxy directly, it currently is ${currentOwner}` ); } else { from = currentOwner; } const oldProxy = proxy.abi.find( (frag: {name: string}) => frag.name === 'changeImplementation' ); if (oldProxy) { upgradeMethod = 'changeImplementation'; upgradeArgsTemplate = ['{implementation}', '{data}']; } const proxyAddress = proxy.address; const upgradeArgs = replaceTemplateArgs(upgradeArgsTemplate, { implementationAddress: implementation.address, proxyAdmin, data, proxyAddress, }); if (!upgradeMethod) { throw new Error(`No upgrade method found, cannot make upgrades`); } let executeReceipt; if (proxyAdminName) { if (oldProxy) { throw new Error(`Old Proxy do not support Proxy Admin contracts`); } if (!currentProxyAdminOwner) { throw new Error(`no currentProxyAdminOwner found in ProxyAdmin`); } executeReceipt = await execute( proxyAdminName, {...options, from: currentProxyAdminOwner}, upgradeMethod, ...upgradeArgs ); } else { executeReceipt = await execute( name, {...options, from}, upgradeMethod, ...upgradeArgs ); } if (!executeReceipt) { throw new Error(`could not execute ${upgradeMethod}`); } } const proxiedDeployment: DeploymentSubmission = { ...proxyContract, receipt: proxy.receipt, address: proxy.address, linkedData: options.linkedData, abi: mergedABI, implementation: implementation.address, args: proxy.args, execute: updateMethod ? {