@ -1,4 +1,4 @@
import { debug } from 'debug' ;
import debug , { Debugger } from 'debug' ;
import { ethers } from 'ethers' ;
import {
@ -12,13 +12,9 @@ import {
IMultisigIsm ,
IMultisigIsm__factory ,
IRoutingIsm ,
IRoutingIsm__factory ,
MailboxClient__factory ,
OPStackIsm ,
OPStackIsm__factory ,
PausableIsm__factory ,
StaticAddressSetFactory ,
StaticAggregationIsm__factory ,
StaticThresholdAddressSetFactory ,
TestIsm__factory ,
} from '@hyperlane-xyz/core' ;
@ -26,10 +22,7 @@ import {
Address ,
Domain ,
eqAddress ,
formatMessage ,
normalizeAddress ,
objFilter ,
objMap ,
warn ,
} from '@hyperlane-xyz/utils' ;
@ -39,12 +32,12 @@ import {
hyperlaneEnvironments ,
} from '../consts/environments' ;
import { appFromAddressesMapHelper } from '../contracts/contracts' ;
import { HyperlaneAddressesMap , HyperlaneContracts } from '../contracts/types' ;
import { HyperlaneAddressesMap } from '../contracts/types' ;
import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer' ;
import {
ProxyFactoryFactories ,
proxyFactoryFactories ,
} from '../deploy/contracts' ;
import { logger } from '../logger' ;
import { MultiProvider } from '../providers/MultiProvider' ;
import { ChainMap , ChainName } from '../types' ;
@ -54,13 +47,11 @@ import {
DeployedIsmType ,
IsmConfig ,
IsmType ,
ModuleType ,
MultisigIsmConfig ,
OpStackIsmConfig ,
RoutingIsmConfig ,
RoutingIsmDelta ,
ismTypeToModuleType ,
} from './types' ;
import { routingModuleDelta } from './utils' ;
export class HyperlaneIsmFactory extends HyperlaneApp < ProxyFactoryFactories > {
// The shape of this object is `ChainMap<Address | ChainMap<Address>`,
@ -68,6 +59,11 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
// TODO: fix this in the next refactoring
public deployedIsms : ChainMap < any > = { } ;
protected deployer? : HyperlaneDeployer < any , any > ;
setDeployer ( deployer : HyperlaneDeployer < any , any > ) : void {
this . deployer = deployer ;
}
static fromEnvironment < Env extends HyperlaneEnvironment > (
env : Env ,
multiProvider : MultiProvider ,
@ -113,7 +109,9 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
}
const ismType = config . type ;
this . logger (
const logger = this . logger . extend ( ` ${ destination } : ${ ismType } ` ) ;
logger (
` Deploying ${ ismType } to ${ destination } ${
origin ? ` (for verifying ${ origin } ) ` : ''
} ` ,
@ -123,7 +121,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
switch ( ismType ) {
case IsmType . MESSAGE_ID_MULTISIG :
case IsmType . MERKLE_ROOT_MULTISIG :
contract = await this . deployMultisigIsm ( destination , config ) ;
contract = await this . deployMultisigIsm ( destination , config , logger ) ;
break ;
case IsmType . ROUTING :
case IsmType . FALLBACK_ROUTING :
@ -133,6 +131,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
origin ,
mailbox ,
existingIsmAddress ,
logger ,
} ) ;
break ;
case IsmType . AGGREGATION :
@ -141,22 +140,39 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
config ,
origin ,
mailbox ,
logger ,
} ) ;
break ;
case IsmType . OP_STACK :
contract = await this . deployOpStackIsm ( destination , config ) ;
if ( ! this . deployer ) {
throw new Error ( ` HyperlaneDeployer must be set to deploy ${ ismType } ` ) ;
}
contract = await this . deployer ? . deployContractFromFactory (
destination ,
new OPStackIsm__factory ( ) ,
IsmType . OP_STACK ,
[ config . nativeBridge ] ,
) ;
break ;
case IsmType . PAUSABLE :
contract = await this . multiProvider . handleDeploy (
if ( ! this . deployer ) {
throw new Error ( ` HyperlaneDeployer must be set to deploy ${ ismType } ` ) ;
}
contract = await this . deployer ? . deployContractFromFactory (
destination ,
new PausableIsm__factory ( ) ,
IsmType . PAUSABLE ,
[ config . owner ] ,
) ;
break ;
case IsmType . TEST_ISM :
contract = await this . multiProvider . handleDeploy (
if ( ! this . deployer ) {
throw new Error ( ` HyperlaneDeployer must be set to deploy ${ ismType } ` ) ;
}
contract = await this . deployer ? . deployContractFromFactory (
destination ,
new TestIsm__factory ( ) ,
IsmType . TEST_ISM ,
[ ] ,
) ;
break ;
@ -185,6 +201,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
protected async deployMultisigIsm (
destination : ChainName ,
config : MultisigIsmConfig ,
logger : Debugger ,
) : Promise < IMultisigIsm > {
const signer = this . multiProvider . getSigner ( destination ) ;
const multisigIsmFactory =
@ -196,6 +213,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
destination ,
multisigIsmFactory ,
config . validators ,
logger ,
config . threshold ,
) ;
@ -208,6 +226,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
origin? : ChainName ;
mailbox? : Address ;
existingIsmAddress? : Address ;
logger : Debugger ;
} ) : Promise < IRoutingIsm > {
const { destination , config , mailbox , existingIsmAddress } = params ;
const overrides = this . multiProvider . getTransactionOverrides ( destination ) ;
@ -263,7 +282,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
// deploying all the ISMs which have to be updated
for ( const originDomain of delta . domainsToEnroll ) {
const origin = this . multiProvider . getChainName ( originDomain ) ; // already filtered to only include domains in the multiprovider
logger (
params . logger (
` Reconfiguring preexisting routing ISM at for origin ${ origin } ... ` ,
) ;
const ism = await this . deploy ( {
@ -282,7 +301,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
}
// unenrolling domains if needed
for ( const originDomain of delta . domainsToUnenroll ) {
logger (
params . logger (
` Unenrolling originDomain ${ originDomain } from preexisting routing ISM at ${ existingIsmAddress } ... ` ,
) ;
const tx = await routingIsm . remove ( originDomain , overrides ) ;
@ -290,7 +309,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
}
// transfer ownership if needed
if ( delta . owner ) {
logger ( ` Transferring ownership of routing ISM... ` ) ;
params . logger ( ` Transferring ownership of routing ISM... ` ) ;
const tx = await routingIsm . transferOwnership ( delta . owner , overrides ) ;
await this . multiProvider . handleTx ( destination , tx ) ;
}
@ -314,13 +333,13 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
'Mailbox address is required for deploying fallback routing ISM' ,
) ;
}
logger ( 'Deploying fallback routing ISM ...' ) ;
params . logger ( 'Deploying fallback routing ISM ...' ) ;
routingIsm = await this . multiProvider . handleDeploy (
destination ,
new DefaultFallbackRoutingIsm__factory ( ) ,
[ mailbox ] ,
) ;
logger ( 'Initialising fallback routing ISM ...' ) ;
params . logger ( 'Initialising fallback routing ISM ...' ) ;
receipt = await this . multiProvider . handleTx (
destination ,
routingIsm [ 'initialize(address,uint32[],address[])' ] (
@ -371,6 +390,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
config : AggregationIsmConfig ;
origin? : ChainName ;
mailbox? : Address ;
logger : Debugger ;
} ) : Promise < IAggregationIsm > {
const { destination , config , origin , mailbox } = params ;
const signer = this . multiProvider . getSigner ( destination ) ;
@ -390,26 +410,17 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
destination ,
aggregationIsmFactory ,
addresses ,
params . logger ,
config . threshold ,
) ;
return IAggregationIsm__factory . connect ( address , signer ) ;
}
protected async deployOpStackIsm (
chain : ChainName ,
config : OpStackIsmConfig ,
) : Promise < OPStackIsm > {
return await this . multiProvider . handleDeploy (
chain ,
new OPStackIsm__factory ( ) ,
[ config . nativeBridge ] ,
) ;
}
async deployStaticAddressSet (
chain : ChainName ,
factory : StaticThresholdAddressSetFactory | StaticAddressSetFactory ,
values : Address [ ] ,
logger : Debugger ,
threshold = values . length ,
) : Promise < Address > {
const sorted = [ . . . values ] . sort ( ) ;
@ -420,7 +431,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
) ;
const code = await this . multiProvider . getProvider ( chain ) . getCode ( address ) ;
if ( code === '0x' ) {
this . logger (
logger (
` Deploying new ${ threshold } of ${ values . length } address set to ${ chain } ` ,
) ;
const overrides = this . multiProvider . getTransactionOverrides ( chain ) ;
@ -432,390 +443,10 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
await this . multiProvider . handleTx ( chain , hash ) ;
// TODO: add proxy verification artifact?
} else {
this . logger (
logger (
` Recovered ${ threshold } of ${ values . length } address set on ${ chain } ` ,
) ;
}
return address ;
}
}
// Note that this function may return false negatives, but should
// not return false positives.
// This can happen if, for example, the module has sender, recipient, or
// body specific logic, as the sample message used when querying the ISM
// sets all of these to zero.
export async function module CanCertainlyVerify (
destModule : Address | IsmConfig ,
multiProvider : MultiProvider ,
origin : ChainName ,
destination : ChainName ,
) : Promise < boolean > {
const originDomainId = multiProvider . tryGetDomainId ( origin ) ;
const destinationDomainId = multiProvider . tryGetDomainId ( destination ) ;
if ( ! originDomainId || ! destinationDomainId ) {
return false ;
}
const message = formatMessage (
0 ,
0 ,
originDomainId ,
ethers . constants . AddressZero ,
destinationDomainId ,
ethers . constants . AddressZero ,
'0x' ,
) ;
const provider = multiProvider . getSignerOrProvider ( destination ) ;
if ( typeof destModule === 'string' ) {
const module = IInterchainSecurityModule__factory . connect (
destModule ,
provider ,
) ;
try {
const module Type = await module .moduleType ( ) ;
if (
module Type === ModuleType . MERKLE_ROOT_MULTISIG ||
module Type === ModuleType . MESSAGE_ID_MULTISIG
) {
const multisigModule = IMultisigIsm__factory . connect (
destModule ,
provider ,
) ;
const [ , threshold ] = await multisigModule . validatorsAndThreshold (
message ,
) ;
return threshold > 0 ;
} else if ( module Type === ModuleType . ROUTING ) {
const routingIsm = IRoutingIsm__factory . connect ( destModule , provider ) ;
const subModule = await routingIsm . route ( message ) ;
return module CanCertainlyVerify (
subModule ,
multiProvider ,
origin ,
destination ,
) ;
} else if ( module Type === ModuleType . AGGREGATION ) {
const aggregationIsm = IAggregationIsm__factory . connect (
destModule ,
provider ,
) ;
const [ subModules , threshold ] =
await aggregationIsm . module sAndThreshold ( message ) ;
let verified = 0 ;
for ( const subModule of subModules ) {
const canVerify = await module CanCertainlyVerify (
subModule ,
multiProvider ,
origin ,
destination ,
) ;
if ( canVerify ) {
verified += 1 ;
}
}
return verified >= threshold ;
} else {
throw new Error ( ` Unsupported module type: ${ module Type } ` ) ;
}
} catch ( e ) {
logger ( ` Error checking module ${ destModule } : ${ e } ` ) ;
return false ;
}
} else {
// destModule is an IsmConfig
switch ( destModule . type ) {
case IsmType . MERKLE_ROOT_MULTISIG :
case IsmType . MESSAGE_ID_MULTISIG :
return destModule . threshold > 0 ;
case IsmType . ROUTING : {
const checking = module CanCertainlyVerify (
destModule . domains [ destination ] ,
multiProvider ,
origin ,
destination ,
) ;
return checking ;
}
case IsmType . AGGREGATION : {
let verified = 0 ;
for ( const subModule of destModule . module s ) {
const canVerify = await module CanCertainlyVerify (
subModule ,
multiProvider ,
origin ,
destination ,
) ;
if ( canVerify ) {
verified += 1 ;
}
}
return verified >= destModule . threshold ;
}
case IsmType . OP_STACK :
return destModule . nativeBridge !== ethers . constants . AddressZero ;
case IsmType . TEST_ISM : {
return true ;
}
default :
throw new Error ( ` Unsupported module type: ${ ( destModule as any ) . type } ` ) ;
}
}
}
export async function module MatchesConfig (
chain : ChainName ,
module Address : Address ,
config : IsmConfig ,
multiProvider : MultiProvider ,
contracts : HyperlaneContracts < ProxyFactoryFactories > ,
mailbox? : Address ,
) : Promise < boolean > {
if ( typeof config === 'string' ) {
return eqAddress ( module Address , config ) ;
}
// If the module address is zero, it can't match any object-based config.
// The subsequent check of what moduleType it is will throw, so we fail here.
if ( eqAddress ( module Address , ethers . constants . AddressZero ) ) {
return false ;
}
const provider = multiProvider . getProvider ( chain ) ;
const module = IInterchainSecurityModule__factory . connect (
module Address ,
provider ,
) ;
const actualType = await module .moduleType ( ) ;
if ( actualType !== ismTypeToModuleType ( config . type ) ) return false ;
let matches = true ;
switch ( config . type ) {
case IsmType . MERKLE_ROOT_MULTISIG : {
// A MerkleRootMultisigIsm matches if validators and threshold match the config
const expectedAddress =
await contracts . merkleRootMultisigIsmFactory . getAddress (
config . validators . sort ( ) ,
config . threshold ,
) ;
matches = eqAddress ( expectedAddress , module .address ) ;
break ;
}
case IsmType . MESSAGE_ID_MULTISIG : {
// A MessageIdMultisigIsm matches if validators and threshold match the config
const expectedAddress =
await contracts . messageIdMultisigIsmFactory . getAddress (
config . validators . sort ( ) ,
config . threshold ,
) ;
matches = eqAddress ( expectedAddress , module .address ) ;
break ;
}
case IsmType . FALLBACK_ROUTING :
case IsmType . ROUTING : {
// A RoutingIsm matches if:
// 1. The set of domains in the config equals those on-chain
// 2. The modules for each domain match the config
// TODO: Check (1)
const routingIsm = DomainRoutingIsm__factory . connect (
module Address ,
provider ,
) ;
// Check that the RoutingISM owner matches the config
const owner = await routingIsm . owner ( ) ;
matches && = eqAddress ( owner , config . owner ) ;
// check if the mailbox matches the config for fallback routing
if ( config . type === IsmType . FALLBACK_ROUTING ) {
const client = MailboxClient__factory . connect ( module Address , provider ) ;
const mailboxAddress = await client . mailbox ( ) ;
matches =
matches &&
mailbox !== undefined &&
eqAddress ( mailboxAddress , mailbox ) ;
}
const delta = await routingModuleDelta (
chain ,
module Address ,
config ,
multiProvider ,
contracts ,
mailbox ,
) ;
matches =
matches &&
delta . domainsToEnroll . length === 0 &&
delta . domainsToUnenroll . length === 0 &&
! delta . mailbox &&
! delta . owner ;
break ;
}
case IsmType . AGGREGATION : {
// An AggregationIsm matches if:
// 1. The threshold matches the config
// 2. There is a bijection between on and off-chain configured modules
const aggregationIsm = StaticAggregationIsm__factory . connect (
module Address ,
provider ,
) ;
const [ subModules , threshold ] = await aggregationIsm . module sAndThreshold (
'0x' ,
) ;
matches && = threshold === config . threshold ;
matches && = subModules . length === config . module s.length ;
const configIndexMatched = new Map ( ) ;
for ( const subModule of subModules ) {
const subModuleMatchesConfig = await Promise . all (
config . module s.map ( ( c ) = >
module MatchesConfig ( chain , subModule , c , multiProvider , contracts ) ,
) ,
) ;
// The submodule returned by the ISM must match exactly one
// entry in the config.
const count = subModuleMatchesConfig . filter ( Boolean ) . length ;
matches && = count === 1 ;
// That entry in the config should not have been matched already.
subModuleMatchesConfig . forEach ( ( matched , index ) = > {
if ( matched ) {
matches && = ! configIndexMatched . has ( index ) ;
configIndexMatched . set ( index , true ) ;
}
} ) ;
}
break ;
}
case IsmType . OP_STACK : {
const opStackIsm = OPStackIsm__factory . connect ( module Address , provider ) ;
const type = await opStackIsm . module Type ( ) ;
matches && = type === ModuleType . NULL ;
break ;
}
case IsmType . TEST_ISM : {
// This is just a TestISM
matches = true ;
break ;
}
case IsmType . PAUSABLE : {
const pausableIsm = PausableIsm__factory . connect ( module Address , provider ) ;
const owner = await pausableIsm . owner ( ) ;
matches && = eqAddress ( owner , config . owner ) ;
if ( config . paused ) {
const isPaused = await pausableIsm . paused ( ) ;
matches && = config . paused === isPaused ;
}
break ;
}
default : {
throw new Error ( 'Unsupported ModuleType' ) ;
}
}
return matches ;
}
export async function routingModuleDelta (
destination : ChainName ,
module Address : Address ,
config : RoutingIsmConfig ,
multiProvider : MultiProvider ,
contracts : HyperlaneContracts < ProxyFactoryFactories > ,
mailbox? : Address ,
) : Promise < RoutingIsmDelta > {
const provider = multiProvider . getProvider ( destination ) ;
const routingIsm = DomainRoutingIsm__factory . connect ( module Address , provider ) ;
const owner = await routingIsm . owner ( ) ;
const deployedDomains = ( await routingIsm . domains ( ) ) . map ( ( domain ) = >
domain . toNumber ( ) ,
) ;
// config.domains is already filtered to only include domains in the multiprovider
const safeConfigDomains = objMap ( config . domains , ( domain ) = >
multiProvider . getDomainId ( domain ) ,
) ;
const delta : RoutingIsmDelta = {
domainsToUnenroll : [ ] ,
domainsToEnroll : [ ] ,
} ;
// if owners don't match, we need to transfer ownership
if ( ! eqAddress ( owner , normalizeAddress ( config . owner ) ) )
delta . owner = config . owner ;
if ( config . type === IsmType . FALLBACK_ROUTING ) {
const client = MailboxClient__factory . connect ( module Address , provider ) ;
const mailboxAddress = await client . mailbox ( ) ;
if ( mailbox && ! eqAddress ( mailboxAddress , mailbox ) ) delta . mailbox = mailbox ;
}
// check for exclusion of domains in the config
delta . domainsToUnenroll = deployedDomains . filter (
( domain ) = > ! Object . values ( safeConfigDomains ) . includes ( domain ) ,
) ;
// check for inclusion of domains in the config
for ( const [ origin , subConfig ] of Object . entries ( config . domains ) ) {
const originDomain = safeConfigDomains [ origin ] ;
if ( ! deployedDomains . includes ( originDomain ) ) {
delta . domainsToEnroll . push ( originDomain ) ;
} else {
const subModule = await routingIsm . module ( originDomain ) ;
// Recursively check that the submodule for each configured
// domain matches the submodule config.
const subModuleMatches = await module MatchesConfig (
destination ,
subModule ,
subConfig ,
multiProvider ,
contracts ,
mailbox ,
) ;
if ( ! subModuleMatches ) delta . domainsToEnroll . push ( originDomain ) ;
}
}
return delta ;
}
export function collectValidators (
origin : ChainName ,
config : IsmConfig ,
) : Set < string > {
// TODO: support address configurations in collectValidators
if ( typeof config === 'string' ) {
debug ( 'hyperlane:IsmFactory' ) (
'Address config unimplemented in collectValidators' ,
) ;
return new Set ( [ ] ) ;
}
let validators : string [ ] = [ ] ;
if (
config . type === IsmType . MERKLE_ROOT_MULTISIG ||
config . type === IsmType . MESSAGE_ID_MULTISIG
) {
validators = config . validators ;
} else if ( config . type === IsmType . ROUTING ) {
if ( Object . keys ( config . domains ) . includes ( origin ) ) {
const domainValidators = collectValidators (
origin ,
config . domains [ origin ] ,
) ;
validators = [ . . . domainValidators ] ;
}
} else if ( config . type === IsmType . AGGREGATION ) {
const aggregatedValidators = config . module s.map ( ( c ) = >
collectValidators ( origin , c ) ,
) ;
aggregatedValidators . forEach ( ( set ) = > {
validators = validators . concat ( [ . . . set ] ) ;
} ) ;
} else if (
config . type === IsmType . TEST_ISM ||
config . type === IsmType . PAUSABLE
) {
return new Set ( [ ] ) ;
} else {
throw new Error ( 'Unsupported ModuleType' ) ;
}
return new Set ( validators ) ;
}