refactor: dedupe HyperlaneIsmFactory and IsmModule.create (#4732)
### Description - Uses HyperlaneIsmFactory in IsmModuleCreate for deduping redundant code ### Backward compatibility Yes ### Testing Unit testspull/4772/head
parent
c622bfbcf5
commit
e104cf6aa3
@ -0,0 +1,6 @@ |
|||||||
|
--- |
||||||
|
'@hyperlane-xyz/utils': patch |
||||||
|
'@hyperlane-xyz/sdk': patch |
||||||
|
--- |
||||||
|
|
||||||
|
Dedupe internals of hook and ISM module deploy code |
@ -1,359 +0,0 @@ |
|||||||
import { ethers } from 'ethers'; |
|
||||||
import { Logger } from 'pino'; |
|
||||||
|
|
||||||
import { |
|
||||||
Ownable__factory, |
|
||||||
StaticAddressSetFactory, |
|
||||||
StaticThresholdAddressSetFactory, |
|
||||||
TransparentUpgradeableProxy__factory, |
|
||||||
} from '@hyperlane-xyz/core'; |
|
||||||
import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; |
|
||||||
import { |
|
||||||
Address, |
|
||||||
addBufferToGasLimit, |
|
||||||
eqAddress, |
|
||||||
rootLogger, |
|
||||||
} from '@hyperlane-xyz/utils'; |
|
||||||
|
|
||||||
import { HyperlaneContracts, HyperlaneFactories } from '../contracts/types.js'; |
|
||||||
import { MultiProvider } from '../providers/MultiProvider.js'; |
|
||||||
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; |
|
||||||
import { ChainMap, ChainName } from '../types.js'; |
|
||||||
|
|
||||||
import { isProxy, proxyConstructorArgs } from './proxy.js'; |
|
||||||
import { ContractVerifier } from './verify/ContractVerifier.js'; |
|
||||||
import { |
|
||||||
ContractVerificationInput, |
|
||||||
ExplorerLicenseType, |
|
||||||
} from './verify/types.js'; |
|
||||||
import { getContractVerificationInput } from './verify/utils.js'; |
|
||||||
|
|
||||||
export class EvmModuleDeployer<Factories extends HyperlaneFactories> { |
|
||||||
public verificationInputs: ChainMap<ContractVerificationInput[]> = {}; |
|
||||||
|
|
||||||
constructor( |
|
||||||
protected readonly multiProvider: MultiProvider, |
|
||||||
protected readonly factories: Factories, |
|
||||||
protected readonly logger = rootLogger.child({ |
|
||||||
module: 'EvmModuleDeployer', |
|
||||||
}), |
|
||||||
protected readonly contractVerifier?: ContractVerifier, |
|
||||||
) { |
|
||||||
this.contractVerifier ??= new ContractVerifier( |
|
||||||
multiProvider, |
|
||||||
{}, |
|
||||||
coreBuildArtifact, |
|
||||||
ExplorerLicenseType.MIT, |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
// Deploys a contract from a factory
|
|
||||||
public async deployContractFromFactory<F extends ethers.ContractFactory>({ |
|
||||||
chain, |
|
||||||
factory, |
|
||||||
contractName, |
|
||||||
constructorArgs, |
|
||||||
initializeArgs, |
|
||||||
implementationAddress, |
|
||||||
}: { |
|
||||||
chain: ChainName; |
|
||||||
factory: F; |
|
||||||
contractName: string; |
|
||||||
constructorArgs: Parameters<F['deploy']>; |
|
||||||
initializeArgs?: Parameters<Awaited<ReturnType<F['deploy']>>['initialize']>; |
|
||||||
implementationAddress?: Address; |
|
||||||
}): Promise<ReturnType<F['deploy']>> { |
|
||||||
this.logger.info( |
|
||||||
`Deploying ${contractName} on ${chain} with constructor args (${constructorArgs.join( |
|
||||||
', ', |
|
||||||
)})...`,
|
|
||||||
); |
|
||||||
const contract = await this.multiProvider.handleDeploy( |
|
||||||
chain, |
|
||||||
factory, |
|
||||||
constructorArgs, |
|
||||||
); |
|
||||||
|
|
||||||
if (initializeArgs) { |
|
||||||
this.logger.debug(`Initialize ${contractName} on ${chain}`); |
|
||||||
// Estimate gas for the initialize transaction
|
|
||||||
const estimatedGas = await contract.estimateGas.initialize( |
|
||||||
...initializeArgs, |
|
||||||
); |
|
||||||
|
|
||||||
// deploy with buffer on gas limit
|
|
||||||
const overrides = this.multiProvider.getTransactionOverrides(chain); |
|
||||||
const initTx = await contract.initialize(...initializeArgs, { |
|
||||||
gasLimit: addBufferToGasLimit(estimatedGas), |
|
||||||
...overrides, |
|
||||||
}); |
|
||||||
|
|
||||||
await this.multiProvider.handleTx(chain, initTx); |
|
||||||
} |
|
||||||
|
|
||||||
const verificationInput = getContractVerificationInput({ |
|
||||||
name: contractName, |
|
||||||
contract, |
|
||||||
bytecode: factory.bytecode, |
|
||||||
expectedimplementation: implementationAddress, |
|
||||||
}); |
|
||||||
this.addVerificationArtifacts({ chain, artifacts: [verificationInput] }); |
|
||||||
|
|
||||||
// try verifying contract
|
|
||||||
try { |
|
||||||
await this.contractVerifier?.verifyContract(chain, verificationInput); |
|
||||||
} catch (error) { |
|
||||||
// log error but keep deploying, can also verify post-deployment if needed
|
|
||||||
this.logger.debug(`Error verifying contract: ${error}`); |
|
||||||
} |
|
||||||
|
|
||||||
return contract; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Deploys a contract with a specified name. |
|
||||||
* |
|
||||||
* This function is capable of deploying any contract type defined within the `Factories` type to a specified chain. |
|
||||||
* |
|
||||||
* @param {ChainName} chain - The name of the chain on which the contract is to be deployed. |
|
||||||
* @param {K} contractKey - The key identifying the factory to use for deployment. |
|
||||||
* @param {string} contractName - The name of the contract to deploy. This must match the contract source code. |
|
||||||
* @param {Parameters<Factories[K]['deploy']>} constructorArgs - Arguments for the contract's constructor. |
|
||||||
* @param {Parameters<Awaited<ReturnType<Factories[K]['deploy']>>['initialize']>?} initializeArgs - Optional arguments for the contract's initialization function. |
|
||||||
* @returns {Promise<HyperlaneContracts<Factories>[K]>} A promise that resolves to the deployed contract instance. |
|
||||||
*/ |
|
||||||
public async deployContractWithName<K extends keyof Factories>({ |
|
||||||
chain, |
|
||||||
contractKey, |
|
||||||
contractName, |
|
||||||
constructorArgs, |
|
||||||
initializeArgs, |
|
||||||
}: { |
|
||||||
chain: ChainName; |
|
||||||
contractKey: K; |
|
||||||
contractName: string; |
|
||||||
constructorArgs: Parameters<Factories[K]['deploy']>; |
|
||||||
initializeArgs?: Parameters< |
|
||||||
Awaited<ReturnType<Factories[K]['deploy']>>['initialize'] |
|
||||||
>; |
|
||||||
}): Promise<HyperlaneContracts<Factories>[K]> { |
|
||||||
const contract = await this.deployContractFromFactory({ |
|
||||||
chain, |
|
||||||
factory: this.factories[contractKey], |
|
||||||
contractName, |
|
||||||
constructorArgs, |
|
||||||
initializeArgs, |
|
||||||
}); |
|
||||||
return contract; |
|
||||||
} |
|
||||||
|
|
||||||
// Deploys a contract with the same name as the contract key
|
|
||||||
public async deployContract<K extends keyof Factories>({ |
|
||||||
chain, |
|
||||||
contractKey, |
|
||||||
constructorArgs, |
|
||||||
initializeArgs, |
|
||||||
}: { |
|
||||||
chain: ChainName; |
|
||||||
contractKey: K; |
|
||||||
constructorArgs: Parameters<Factories[K]['deploy']>; |
|
||||||
initializeArgs?: Parameters< |
|
||||||
Awaited<ReturnType<Factories[K]['deploy']>>['initialize'] |
|
||||||
>; |
|
||||||
}): Promise<HyperlaneContracts<Factories>[K]> { |
|
||||||
return this.deployContractWithName({ |
|
||||||
chain, |
|
||||||
contractKey, |
|
||||||
contractName: contractKey.toString(), |
|
||||||
constructorArgs, |
|
||||||
initializeArgs, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
// Deploys the Implementation and Proxy for a given contract
|
|
||||||
public async deployProxiedContract<K extends keyof Factories>({ |
|
||||||
chain, |
|
||||||
contractKey, |
|
||||||
contractName, |
|
||||||
proxyAdmin, |
|
||||||
constructorArgs, |
|
||||||
initializeArgs, |
|
||||||
}: { |
|
||||||
chain: ChainName; |
|
||||||
contractKey: K; |
|
||||||
contractName: string; |
|
||||||
proxyAdmin: string; |
|
||||||
constructorArgs: Parameters<Factories[K]['deploy']>; |
|
||||||
initializeArgs?: Parameters<HyperlaneContracts<Factories>[K]['initialize']>; |
|
||||||
}): Promise<HyperlaneContracts<Factories>[K]> { |
|
||||||
// Try to initialize the implementation even though it may not be necessary
|
|
||||||
const implementation = await this.deployContractWithName({ |
|
||||||
chain, |
|
||||||
contractKey, |
|
||||||
contractName, |
|
||||||
constructorArgs, |
|
||||||
initializeArgs, |
|
||||||
}); |
|
||||||
|
|
||||||
// Initialize the proxy the same way
|
|
||||||
return this.deployProxy({ |
|
||||||
chain, |
|
||||||
implementation, |
|
||||||
proxyAdmin, |
|
||||||
initializeArgs, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
// Deploys a proxy for a given implementation contract
|
|
||||||
protected async deployProxy<C extends ethers.Contract>({ |
|
||||||
chain, |
|
||||||
implementation, |
|
||||||
proxyAdmin, |
|
||||||
initializeArgs, |
|
||||||
}: { |
|
||||||
chain: ChainName; |
|
||||||
implementation: C; |
|
||||||
proxyAdmin: string; |
|
||||||
initializeArgs?: Parameters<C['initialize']>; |
|
||||||
}): Promise<C> { |
|
||||||
const isProxied = await isProxy( |
|
||||||
this.multiProvider.getProvider(chain), |
|
||||||
implementation.address, |
|
||||||
); |
|
||||||
if (isProxied) { |
|
||||||
// if the implementation is already a proxy, do not deploy a new proxy
|
|
||||||
return implementation; |
|
||||||
} |
|
||||||
|
|
||||||
const constructorArgs = proxyConstructorArgs( |
|
||||||
implementation, |
|
||||||
proxyAdmin, |
|
||||||
initializeArgs, |
|
||||||
); |
|
||||||
const proxy = await this.deployContractFromFactory({ |
|
||||||
chain, |
|
||||||
factory: new TransparentUpgradeableProxy__factory(), |
|
||||||
contractName: 'TransparentUpgradeableProxy', |
|
||||||
constructorArgs, |
|
||||||
implementationAddress: implementation.address, |
|
||||||
}); |
|
||||||
|
|
||||||
return implementation.attach(proxy.address) as C; |
|
||||||
} |
|
||||||
|
|
||||||
// Adds verification artifacts to the verificationInputs map
|
|
||||||
protected addVerificationArtifacts({ |
|
||||||
chain, |
|
||||||
artifacts, |
|
||||||
}: { |
|
||||||
chain: ChainName; |
|
||||||
artifacts: ContractVerificationInput[]; |
|
||||||
}): void { |
|
||||||
this.verificationInputs[chain] = this.verificationInputs[chain] || []; |
|
||||||
artifacts.forEach((artifact) => { |
|
||||||
this.verificationInputs[chain].push(artifact); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
// Static deploy function used by Hook and ISM modules.
|
|
||||||
public static async deployStaticAddressSet({ |
|
||||||
chain, |
|
||||||
factory, |
|
||||||
values, |
|
||||||
logger, |
|
||||||
threshold = values.length, |
|
||||||
multiProvider, |
|
||||||
}: { |
|
||||||
chain: ChainName; |
|
||||||
factory: StaticThresholdAddressSetFactory | StaticAddressSetFactory; |
|
||||||
values: Address[]; |
|
||||||
logger: Logger; |
|
||||||
threshold?: number; |
|
||||||
multiProvider: MultiProvider; |
|
||||||
}): Promise<Address> { |
|
||||||
const sortedValues = [...values].sort(); |
|
||||||
|
|
||||||
const address = await factory['getAddress(address[],uint8)']( |
|
||||||
sortedValues, |
|
||||||
threshold, |
|
||||||
); |
|
||||||
const code = await multiProvider.getProvider(chain).getCode(address); |
|
||||||
if (code === '0x') { |
|
||||||
logger.debug( |
|
||||||
`Deploying new ${threshold} of ${sortedValues.length} address set to ${chain}`, |
|
||||||
); |
|
||||||
const overrides = multiProvider.getTransactionOverrides(chain); |
|
||||||
|
|
||||||
// estimate gas
|
|
||||||
const estimatedGas = await factory.estimateGas['deploy(address[],uint8)']( |
|
||||||
sortedValues, |
|
||||||
threshold, |
|
||||||
overrides, |
|
||||||
); |
|
||||||
|
|
||||||
// add gas buffer
|
|
||||||
const hash = await factory['deploy(address[],uint8)']( |
|
||||||
sortedValues, |
|
||||||
threshold, |
|
||||||
{ |
|
||||||
gasLimit: addBufferToGasLimit(estimatedGas), |
|
||||||
...overrides, |
|
||||||
}, |
|
||||||
); |
|
||||||
|
|
||||||
await multiProvider.handleTx(chain, hash); |
|
||||||
} else { |
|
||||||
logger.debug( |
|
||||||
`Recovered ${threshold} of ${sortedValues.length} address set on ${chain}: ${address}`, |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
// TODO: figure out how to get the constructor arguments for manual deploy TXs
|
|
||||||
// const verificationInput = buildVerificationInput(
|
|
||||||
// NAME,
|
|
||||||
// ADDRESS,
|
|
||||||
// CONSTRUCTOR_ARGS,
|
|
||||||
// );
|
|
||||||
// await this.deployer.verifyContract(
|
|
||||||
// this.chainName,
|
|
||||||
// verificationInput,
|
|
||||||
// logger,
|
|
||||||
// );
|
|
||||||
|
|
||||||
return address; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Transfers ownership of a contract to a new owner. |
|
||||||
* |
|
||||||
* @param actualOwner - The current owner of the contract. |
|
||||||
* @param expectedOwner - The expected new owner of the contract. |
|
||||||
* @param deployedAddress - The address of the deployed contract. |
|
||||||
* @param chainId - The chain ID of the network the contract is deployed on. |
|
||||||
* @returns An array of annotated EV5 transactions that need to be executed to update the owner. |
|
||||||
*/ |
|
||||||
public static createTransferOwnershipTx(params: { |
|
||||||
actualOwner: Address; |
|
||||||
expectedOwner: Address; |
|
||||||
deployedAddress: Address; |
|
||||||
chainId: number; |
|
||||||
}): AnnotatedEV5Transaction[] { |
|
||||||
const { actualOwner, expectedOwner, deployedAddress, chainId } = params; |
|
||||||
const updateTransactions: AnnotatedEV5Transaction[] = []; |
|
||||||
if (eqAddress(actualOwner, expectedOwner)) { |
|
||||||
return []; |
|
||||||
} |
|
||||||
|
|
||||||
updateTransactions.push({ |
|
||||||
annotation: `Transferring ownership of ${deployedAddress} from current owner ${actualOwner} to new owner ${expectedOwner}`, |
|
||||||
chainId, |
|
||||||
to: deployedAddress, |
|
||||||
data: Ownable__factory.createInterface().encodeFunctionData( |
|
||||||
'transferOwnership(address)', |
|
||||||
[expectedOwner], |
|
||||||
), |
|
||||||
}); |
|
||||||
|
|
||||||
return updateTransactions; |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue