feat: add sdk support for `IStaticWeightedMultisigIsm` (#4331)

### Description

Add WeightedMerkleRootMultisigIsm and WeightedMessageIdMultisigIsm
support to the proxy factory deployer and the hyperlaneIsmFactory

### Drive-by changes

none

### Related issues

- fixes https://github.com/hyperlane-xyz/issues/issues/1345

### Backward compatibility

Yes

### Testing

Unit
pull/4372/head
Kunal Arora 2 months ago committed by GitHub
parent 3010e36ec9
commit 203084df2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      .changeset/dull-days-yell.md
  2. 4
      solidity/contracts/interfaces/IInterchainSecurityModule.sol
  3. 4
      solidity/contracts/isms/multisig/WeightedMultisigIsm.sol
  4. 8
      typescript/cli/src/deploy/warp.ts
  5. 27
      typescript/infra/config/environments/test/multisigIsm.ts
  6. 4
      typescript/sdk/src/core/EvmCoreModule.ts
  7. 10
      typescript/sdk/src/deploy/contracts.ts
  8. 2
      typescript/sdk/src/deploy/schemas.ts
  9. 1
      typescript/sdk/src/index.ts
  10. 4
      typescript/sdk/src/ism/EvmIsmModule.ts
  11. 35
      typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts
  12. 79
      typescript/sdk/src/ism/HyperlaneIsmFactory.ts
  13. 20
      typescript/sdk/src/ism/schemas.ts
  14. 15
      typescript/sdk/src/ism/types.ts
  15. 18
      typescript/sdk/src/ism/utils.ts

@ -0,0 +1,8 @@
---
'@hyperlane-xyz/infra': minor
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
'@hyperlane-xyz/core': minor
---
Added sdk support for Stake weighted ISM

@ -12,8 +12,8 @@ interface IInterchainSecurityModule {
NULL, // used with relayer carrying no metadata
CCIP_READ,
ARB_L2_TO_L1,
WEIGHT_MERKLE_ROOT_MULTISIG,
WEIGHT_MESSAGE_ID_MULTISIG,
WEIGHTED_MERKLE_ROOT_MULTISIG,
WEIGHTED_MESSAGE_ID_MULTISIG,
OP_L2_TO_L1
}

@ -39,7 +39,7 @@ contract StaticMerkleRootWeightedMultisigIsm is
AbstractMetaProxyWeightedMultisigIsm
{
uint8 public constant moduleType =
uint8(IInterchainSecurityModule.Types.WEIGHT_MERKLE_ROOT_MULTISIG);
uint8(IInterchainSecurityModule.Types.WEIGHTED_MERKLE_ROOT_MULTISIG);
}
contract StaticMessageIdWeightedMultisigIsm is
@ -47,7 +47,7 @@ contract StaticMessageIdWeightedMultisigIsm is
AbstractMetaProxyWeightedMultisigIsm
{
uint8 public constant moduleType =
uint8(IInterchainSecurityModule.Types.WEIGHT_MESSAGE_ID_MULTISIG);
uint8(IInterchainSecurityModule.Types.WEIGHTED_MESSAGE_ID_MULTISIG);
}
contract StaticMerkleRootWeightedMultisigIsmFactory is

@ -278,6 +278,10 @@ async function deployAndResolveWarpIsm(
chainAddresses.staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory:
chainAddresses.staticMessageIdMultisigIsmFactory,
staticMerkleRootWeightedMultisigIsmFactory:
chainAddresses.staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory:
chainAddresses.staticMessageIdWeightedMultisigIsmFactory,
},
contractVerifier,
);
@ -308,6 +312,8 @@ async function createWarpIsm(
staticAggregationIsmFactory,
staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory,
staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory,
} = factoryAddresses;
const evmIsmModule = await EvmIsmModule.create({
chain,
@ -319,6 +325,8 @@ async function createWarpIsm(
staticAggregationIsmFactory,
staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory,
staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory,
},
config: warpConfig[chain].interchainSecurityModule!,
contractVerifier,

@ -3,6 +3,7 @@ import {
IsmType,
MultisigIsmConfig,
TestChainName,
WeightedMultisigIsmConfig,
} from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';
@ -39,3 +40,29 @@ export const multisigIsm: ChainMap<MultisigIsmConfig> = {
test3: messageIdMultisig(chainToValidator['test3']),
test4: messageIdMultisig(chainToValidator['test4']),
};
export const uniformlyWeightedMultisigIsm = (
multisigIsm: MultisigIsmConfig,
): WeightedMultisigIsmConfig => {
const totalWeight = 1e10;
const numValidators = multisigIsm.validators.length;
const baseWeight = Math.floor(totalWeight / numValidators);
const remainingWeight = totalWeight - baseWeight * (numValidators - 1);
const validators = multisigIsm.validators.map((validatorKey, index) => ({
signingAddress: validatorKey,
weight: index === numValidators - 1 ? remainingWeight : baseWeight,
}));
const thresholdWeight = multisigIsm.threshold * baseWeight;
const weightedType =
multisigIsm.type === IsmType.MESSAGE_ID_MULTISIG
? IsmType.WEIGHTED_MESSAGE_ID_MULTISIG
: IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG;
return {
type: weightedType,
validators,
thresholdWeight,
};
};

@ -157,6 +157,8 @@ export class EvmCoreModule extends HyperlaneModule<
staticAggregationHookFactory,
staticMessageIdMultisigIsmFactory,
staticMerkleRootMultisigIsmFactory,
staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory,
} = this.serialize();
const ismModule = new EvmIsmModule(this.multiProvider, {
@ -169,6 +171,8 @@ export class EvmCoreModule extends HyperlaneModule<
staticAggregationHookFactory,
staticMessageIdMultisigIsmFactory,
staticMerkleRootMultisigIsmFactory,
staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory,
deployedIsm: actualDefaultIsmConfig.address,
},
});

@ -3,7 +3,9 @@ import {
StaticAggregationHookFactory__factory,
StaticAggregationIsmFactory__factory,
StaticMerkleRootMultisigIsmFactory__factory,
StaticMerkleRootWeightedMultisigIsmFactory__factory,
StaticMessageIdMultisigIsmFactory__factory,
StaticMessageIdWeightedMultisigIsmFactory__factory,
} from '@hyperlane-xyz/core';
// Any name changes here should also be reflected in the example artifacts.
@ -16,6 +18,10 @@ export const proxyFactoryFactories = {
staticAggregationIsmFactory: new StaticAggregationIsmFactory__factory(),
staticAggregationHookFactory: new StaticAggregationHookFactory__factory(),
domainRoutingIsmFactory: new DomainRoutingIsmFactory__factory(),
staticMerkleRootWeightedMultisigIsmFactory:
new StaticMerkleRootWeightedMultisigIsmFactory__factory(),
staticMessageIdWeightedMultisigIsmFactory:
new StaticMessageIdWeightedMultisigIsmFactory__factory(),
};
export type ProxyFactoryFactories = typeof proxyFactoryFactories;
@ -29,4 +35,8 @@ export const proxyFactoryImplementations: ProxyFactoryImplementations = {
staticAggregationIsmFactory: 'StaticAggregationIsm',
staticAggregationHookFactory: 'StaticAggregationHook',
domainRoutingIsmFactory: 'DomaingRoutingIsm',
staticMerkleRootWeightedMultisigIsmFactory:
'StaticMerkleRootWeightedMultisigIsm',
staticMessageIdWeightedMultisigIsmFactory:
'StaticMessageIdWeightedMultisigIsm',
};

@ -12,6 +12,8 @@ export const ProxyFactoryFactoriesSchema = z.object({
staticAggregationIsmFactory: z.string(),
staticAggregationHookFactory: z.string(),
domainRoutingIsmFactory: z.string(),
staticMerkleRootWeightedMultisigIsmFactory: z.string(),
staticMessageIdWeightedMultisigIsmFactory: z.string(),
});
export type ProxyFactoryFactoriesAddresses = z.infer<

@ -165,6 +165,7 @@ export {
PausableIsmConfig,
RoutingIsmConfig,
TrustedRelayerIsmConfig,
WeightedMultisigIsmConfig,
} from './ism/types.js';
export { collectValidators, moduleCanCertainlyVerify } from './ism/utils.js';
export {

@ -112,6 +112,10 @@ export class EvmIsmModule extends HyperlaneModule<
staticAggregationHookFactory:
params.addresses.staticAggregationHookFactory,
domainRoutingIsmFactory: params.addresses.domainRoutingIsmFactory,
staticMerkleRootWeightedMultisigIsmFactory:
params.addresses.staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory:
params.addresses.staticMessageIdWeightedMultisigIsmFactory,
},
proxyFactoryFactories,
multiProvider.getSigner(params.chain),

@ -21,6 +21,7 @@ import {
MultisigIsmConfig,
RoutingIsmConfig,
TrustedRelayerIsmConfig,
WeightedMultisigIsmConfig,
} from './types.js';
import { moduleMatchesConfig } from './utils.js';
@ -28,6 +29,7 @@ function randomModuleType(): ModuleType {
const choices = [
ModuleType.AGGREGATION,
ModuleType.MESSAGE_ID_MULTISIG,
ModuleType.WEIGHTED_MESSAGE_ID_MULTISIG,
ModuleType.ROUTING,
ModuleType.NULL,
];
@ -50,6 +52,36 @@ const randomMultisigIsmConfig = (
};
};
const randomWeightedMultisigIsmConfig = (
n: number,
addresses?: string[],
): WeightedMultisigIsmConfig => {
const totalWeight = 1e10;
const emptyArray = new Array<number>(n).fill(0);
const validators = emptyArray.map(() => ({
signingAddress: addresses ? randomElement(addresses) : randomAddress(),
weight: 0,
}));
let remainingWeight = totalWeight;
for (let i = 0; i < n; i++) {
if (i === n - 1) {
validators[i].weight = remainingWeight;
} else {
const weight = Math.floor(Math.random() * (remainingWeight + 1));
validators[i].weight = weight;
remainingWeight -= weight;
}
}
const thresholdWeight = Math.floor(Math.random() * totalWeight);
return {
type: IsmType.WEIGHTED_MESSAGE_ID_MULTISIG,
validators,
thresholdWeight,
};
};
export const randomIsmConfig = (
maxDepth = 5,
validatorAddresses?: string[],
@ -60,6 +92,9 @@ export const randomIsmConfig = (
if (moduleType === ModuleType.MESSAGE_ID_MULTISIG) {
const n = randomInt(validatorAddresses?.length ?? 5, 1);
return randomMultisigIsmConfig(randomInt(n, 1), n, validatorAddresses);
} else if (moduleType === ModuleType.WEIGHTED_MESSAGE_ID_MULTISIG) {
const n = randomInt(validatorAddresses?.length ?? 5, 1);
return randomWeightedMultisigIsmConfig(randomInt(n, 1), validatorAddresses);
} else if (moduleType === ModuleType.ROUTING) {
const config: RoutingIsmConfig = {
type: IsmType.ROUTING,

@ -12,10 +12,12 @@ import {
IMultisigIsm,
IMultisigIsm__factory,
IRoutingIsm,
IStaticWeightedMultisigIsm,
OPStackIsm__factory,
PausableIsm__factory,
StaticAddressSetFactory,
StaticThresholdAddressSetFactory,
StaticWeightedValidatorSetFactory,
TestIsm__factory,
TrustedRelayerIsm__factory,
} from '@hyperlane-xyz/core';
@ -48,6 +50,7 @@ import {
MultisigIsmConfig,
RoutingIsmConfig,
RoutingIsmDelta,
WeightedMultisigIsmConfig,
} from './types.js';
import { routingModuleDelta } from './utils.js';
@ -109,6 +112,14 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
case IsmType.MERKLE_ROOT_MULTISIG:
contract = await this.deployMultisigIsm(destination, config, logger);
break;
case IsmType.WEIGHTED_MESSAGE_ID_MULTISIG:
case IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG:
contract = await this.deployWeightedMultisigIsm(
destination,
config,
logger,
);
break;
case IsmType.ROUTING:
case IsmType.FALLBACK_ROUTING:
contract = await this.deployRoutingIsm({
@ -224,6 +235,30 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
return IMultisigIsm__factory.connect(address, signer);
}
protected async deployWeightedMultisigIsm(
destination: ChainName,
config: WeightedMultisigIsmConfig,
logger: Logger,
): Promise<IMultisigIsm> {
const signer = this.multiProvider.getSigner(destination);
const weightedmultisigIsmFactory =
config.type === IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG
? this.getContracts(destination)
.staticMerkleRootWeightedMultisigIsmFactory
: this.getContracts(destination)
.staticMessageIdWeightedMultisigIsmFactory;
const address = await this.deployStaticWeightedValidatorSet(
destination,
weightedmultisigIsmFactory,
config.validators,
config.thresholdWeight,
logger,
);
return IMultisigIsm__factory.connect(address, signer);
}
protected async deployRoutingIsm(params: {
destination: ChainName;
config: RoutingIsmConfig;
@ -476,4 +511,48 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
}
return address;
}
async deployStaticWeightedValidatorSet(
chain: ChainName,
factory: StaticWeightedValidatorSetFactory,
values: IStaticWeightedMultisigIsm.ValidatorInfoStruct[],
thresholdWeight = 66e8,
logger: Logger,
): Promise<Address> {
const sorted = [...values].sort();
const address = await factory['getAddress((address,uint96)[],uint96)'](
sorted,
thresholdWeight,
);
const code = await this.multiProvider.getProvider(chain).getCode(address);
if (code === '0x') {
logger.debug(
`Deploying new weighted set of ${values.length} validators with a threshold weight ${thresholdWeight} on ${chain} `,
);
const overrides = this.multiProvider.getTransactionOverrides(chain);
// estimate gas
const estimatedGas = await factory.estimateGas[
'deploy((address,uint96)[],uint96)'
](sorted, thresholdWeight, overrides);
// add 10% buffer
const hash = await factory['deploy((address,uint96)[],uint96)'](
sorted,
thresholdWeight,
{
...overrides,
gasLimit: estimatedGas.add(estimatedGas.div(10)), // 10% buffer
},
);
await this.multiProvider.handleTx(chain, hash);
// TODO: add proxy verification artifact?
} else {
logger.debug(
`Recovered weighted set of ${values.length} validators on ${chain} with a threshold weight ${thresholdWeight}: ${address}`,
);
}
return address;
}
}

@ -5,6 +5,11 @@ import { OwnableSchema, PausableSchema } from '../schemas.js';
import { AggregationIsmConfig, IsmType, RoutingIsmConfig } from './types.js';
const ValidatorInfoSchema = z.object({
signingAddress: ZHash,
weight: z.number(),
});
export const TestIsmConfigSchema = z.object({
type: z.literal(IsmType.TEST_ISM),
});
@ -14,6 +19,11 @@ export const MultisigConfigSchema = z.object({
threshold: z.number(),
});
export const WeightedMultisigConfigSchema = z.object({
validators: z.array(ValidatorInfoSchema),
thresholdWeight: z.number(),
});
export const TrustedRelayerIsmConfigSchema = z.object({
type: z.literal(IsmType.TRUSTED_RELAYER),
relayer: z.string(),
@ -45,6 +55,15 @@ export const MultisigIsmConfigSchema = MultisigConfigSchema.and(
}),
);
export const WeightedMultisigIsmConfigSchema = WeightedMultisigConfigSchema.and(
z.object({
type: z.union([
z.literal(IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG),
z.literal(IsmType.WEIGHTED_MESSAGE_ID_MULTISIG),
]),
}),
);
export const RoutingIsmConfigSchema: z.ZodSchema<RoutingIsmConfig> = z.lazy(
() =>
OwnableSchema.extend({
@ -75,6 +94,7 @@ export const IsmConfigSchema = z.union([
PausableIsmConfigSchema,
TrustedRelayerIsmConfigSchema,
MultisigIsmConfigSchema,
WeightedMultisigIsmConfigSchema,
RoutingIsmConfigSchema,
AggregationIsmConfigSchema,
ArbL2ToL1IsmConfigSchema,

@ -6,6 +6,7 @@ import {
IInterchainSecurityModule,
IMultisigIsm,
IRoutingIsm,
IStaticWeightedMultisigIsm,
OPStackIsm,
PausableIsm,
TestIsm,
@ -24,6 +25,7 @@ import {
PausableIsmConfigSchema,
TestIsmConfigSchema,
TrustedRelayerIsmConfigSchema,
WeightedMultisigIsmConfigSchema,
} from './schemas.js';
// this enum should match the IInterchainSecurityModule.sol enum
@ -38,6 +40,8 @@ export enum ModuleType {
NULL,
CCIP_READ,
ARB_L2_TO_L1,
WEIGHTED_MERKLE_ROOT_MULTISIG,
WEIGHTED_MESSAGE_ID_MULTISIG,
}
// this enum can be adjusted as per deployments necessary
@ -54,6 +58,8 @@ export enum IsmType {
PAUSABLE = 'pausableIsm',
TRUSTED_RELAYER = 'trustedRelayerIsm',
ARB_L2_TO_L1 = 'arbL2ToL1Ism',
WEIGHTED_MERKLE_ROOT_MULTISIG = 'weightedMerkleRootMultisigIsm',
WEIGHTED_MESSAGE_ID_MULTISIG = 'weightedMessageIdMultisigIsm',
}
// ISM types that can be updated in-place
@ -84,6 +90,10 @@ export function ismTypeToModuleType(ismType: IsmType): ModuleType {
return ModuleType.NULL;
case IsmType.ARB_L2_TO_L1:
return ModuleType.ARB_L2_TO_L1;
case IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG:
return ModuleType.WEIGHTED_MERKLE_ROOT_MULTISIG;
case IsmType.WEIGHTED_MESSAGE_ID_MULTISIG:
return ModuleType.WEIGHTED_MESSAGE_ID_MULTISIG;
}
}
@ -93,6 +103,9 @@ export type MultisigConfig = {
};
export type MultisigIsmConfig = z.infer<typeof MultisigIsmConfigSchema>;
export type WeightedMultisigIsmConfig = z.infer<
typeof WeightedMultisigIsmConfigSchema
>;
export type TestIsmConfig = z.infer<typeof TestIsmConfigSchema>;
export type PausableIsmConfig = z.infer<typeof PausableIsmConfigSchema>;
export type OpStackIsmConfig = z.infer<typeof OpStackIsmConfigSchema>;
@ -132,6 +145,8 @@ export type DeployedIsmType = {
[IsmType.PAUSABLE]: PausableIsm;
[IsmType.TRUSTED_RELAYER]: TrustedRelayerIsm;
[IsmType.ARB_L2_TO_L1]: ArbL2ToL1Ism;
[IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG]: IStaticWeightedMultisigIsm;
[IsmType.WEIGHTED_MESSAGE_ID_MULTISIG]: IStaticWeightedMultisigIsm;
};
export type DeployedIsm = ValueOf<DeployedIsmType>;

@ -359,6 +359,24 @@ export async function moduleMatchesConfig(
}
break;
}
case IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG: {
const expectedAddress =
await contracts.staticMerkleRootWeightedMultisigIsmFactory.getAddress(
config.validators.sort(),
config.thresholdWeight,
);
matches = eqAddress(expectedAddress, module.address);
break;
}
case IsmType.WEIGHTED_MESSAGE_ID_MULTISIG: {
const expectedAddress =
await contracts.staticMessageIdWeightedMultisigIsmFactory.getAddress(
config.validators.sort(),
config.thresholdWeight,
);
matches = eqAddress(expectedAddress, module.address);
break;
}
default: {
throw new Error('Unsupported ModuleType');
}

Loading…
Cancel
Save