Skip to content

Commit

Permalink
feat: support safe tx parsing for zksync chains (#5042)
Browse files Browse the repository at this point in the history
### Description

1. correctly generate core config for zksync chains
- copied from `pb/zksync`
https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/pb/zksync/typescript/infra/config/environments/mainnet3/core.ts
- note the storage aggregation ism was not around when doing the first
zksync chain deploys, and we haven't gone back and updated the config
generation yet to include this
- required so we infer the correct config for zksync changes and don't
assume it's the same as non-zksync chains

2. update ISM reader to return storagemultisigism types on zksync
	- since static ISMs are not supported on zksync
- the moduleType is the same, so the reader has to determine if it's
static/storage
- note: at the moment on non-zksync we assume it's a static
multisig/aggregation ISM
	- required so we correctly compare the config vs onchain config

### Drive-by changes

<!--
Are there any minor or drive-by changes also included?
-->

### Related issues

<!--
- Fixes #[issue number here]
-->

### Backward compatibility

<!--
Are these changes backward compatible? Are there any infrastructure
implications, e.g. changes that would prohibit deploying older commits
using this infra tooling?

Yes/No
-->

### Testing

- these changes were used to verify SAFE txs for the last couple of
batches
- the config changes have also been used in production on zksync chains
since October 23rd 2024 [`26d198a`
(#4761)](26d198a#diff-d4db62438f3fd9acf24be52093e81d126292859f9f48c81aa1704d41fe8ddf1a)
  • Loading branch information
paulbalaji authored Jan 10, 2025
1 parent fc80df5 commit 79c61c8
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .changeset/eleven-carrots-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': patch
---

Fix the return type of multisig and aggregation ISMs for zksync-stack chains.
90 changes: 70 additions & 20 deletions typescript/infra/config/environments/mainnet3/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {
AggregationHookConfig,
AggregationIsmConfig,
ChainMap,
ChainTechnicalStack,
CoreConfig,
FallbackRoutingHookConfig,
HookType,
IgpConfig,
IsmType,
MerkleTreeHookConfig,
MultisigConfig,
Expand All @@ -20,10 +22,15 @@ import {
} from '@hyperlane-xyz/sdk';
import { Address, objMap } from '@hyperlane-xyz/utils';

import { getChain } from '../../registry.js';

import { igp } from './igp.js';
import { DEPLOYER, ethereumChainOwners } from './owners.js';
import { supportedChainNames } from './supportedChainNames.js';

// There are no static ISMs or hooks for zkSync, this means
// that the default ISM is a routing ISM and the default hook
// is a fallback routing hook.
export const core: ChainMap<CoreConfig> = objMap(
ethereumChainOwners,
(local, owner) => {
Expand All @@ -33,11 +40,26 @@ export const core: ChainMap<CoreConfig> = objMap(
.map((origin) => [origin, defaultMultisigConfigs[origin]]),
);

const isZksyncChain =
getChain(local).technicalStack === ChainTechnicalStack.ZkSync;

// zkSync uses a different ISM for the merkle root
const merkleRoot = (multisig: MultisigConfig): MultisigIsmConfig =>
multisigConfigToIsmConfig(IsmType.MERKLE_ROOT_MULTISIG, multisig);
multisigConfigToIsmConfig(
isZksyncChain
? IsmType.STORAGE_MERKLE_ROOT_MULTISIG
: IsmType.MERKLE_ROOT_MULTISIG,
multisig,
);

// zkSync uses a different ISM for the message ID
const messageIdIsm = (multisig: MultisigConfig): MultisigIsmConfig =>
multisigConfigToIsmConfig(IsmType.MESSAGE_ID_MULTISIG, multisig);
multisigConfigToIsmConfig(
isZksyncChain
? IsmType.STORAGE_MESSAGE_ID_MULTISIG
: IsmType.MESSAGE_ID_MULTISIG,
multisig,
);

const routingIsm: RoutingIsmConfig = {
type: IsmType.ROUTING,
Expand All @@ -52,17 +74,30 @@ export const core: ChainMap<CoreConfig> = objMap(
...owner,
};

// No static aggregation ISM support on zkSync
const defaultZkSyncIsm = (): RoutingIsmConfig => ({
type: IsmType.ROUTING,
domains: objMap(
originMultisigs,
(_, multisig): MultisigIsmConfig => messageIdIsm(multisig),
),
...owner,
});

const pausableIsm: PausableIsmConfig = {
type: IsmType.PAUSABLE,
paused: false,
owner: DEPLOYER, // keep pausable hot
};

const defaultIsm: AggregationIsmConfig = {
type: IsmType.AGGREGATION,
modules: [routingIsm, pausableIsm],
threshold: 2,
};
// No static aggregation ISM support on zkSync
const defaultIsm: AggregationIsmConfig | RoutingIsmConfig = isZksyncChain
? defaultZkSyncIsm()
: {
type: IsmType.AGGREGATION,
modules: [routingIsm, pausableIsm],
threshold: 2,
};

const merkleHook: MerkleTreeHookConfig = {
type: HookType.MERKLE_TREE,
Expand All @@ -75,30 +110,45 @@ export const core: ChainMap<CoreConfig> = objMap(
paused: false,
owner: DEPLOYER, // keep pausable hot
};
const aggregationHooks = objMap(

// No static aggregation hook support on zkSync
const defaultHookDomains = objMap(
originMultisigs,
(_origin, _): AggregationHookConfig => ({
type: HookType.AGGREGATION,
hooks: [pausableHook, merkleHook, igpHook],
}),
(_origin, _): AggregationHookConfig | IgpConfig => {
return isZksyncChain
? igpHook
: {
type: HookType.AGGREGATION,
hooks: [pausableHook, merkleHook, igpHook],
};
},
);

const defaultHook: FallbackRoutingHookConfig = {
type: HookType.FALLBACK_ROUTING,
...owner,
domains: aggregationHooks,
domains: defaultHookDomains,
fallback: merkleHook,
};

if (typeof owner.owner !== 'string') {
throw new Error('beneficiary must be a string');
}
const requiredHook: ProtocolFeeHookConfig = {
type: HookType.PROTOCOL_FEE,
maxProtocolFee: ethers.utils.parseUnits('1', 'gwei').toString(), // 1 gwei of native token
protocolFee: BigNumber.from(0).toString(), // 0 wei
beneficiary: owner.owner as Address, // Owner can be AccountConfig
...owner,
};

// No aggregation hook support on zkSync, so we ignore protocolFee
// and make the merkleTreeHook required
const requiredHook: ProtocolFeeHookConfig | MerkleTreeHookConfig =
isZksyncChain
? {
type: HookType.MERKLE_TREE,
}
: {
type: HookType.PROTOCOL_FEE,
maxProtocolFee: ethers.utils.parseUnits('1', 'gwei').toString(), // 1 gwei of native token
protocolFee: BigNumber.from(0).toString(), // 0 wei
beneficiary: owner.owner as Address, // Owner can be AccountConfig
...owner,
};

return {
defaultIsm,
Expand Down
88 changes: 67 additions & 21 deletions typescript/infra/config/environments/testnet4/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {
AggregationHookConfig,
AggregationIsmConfig,
ChainMap,
ChainTechnicalStack,
CoreConfig,
FallbackRoutingHookConfig,
HookType,
IgpConfig,
IsmType,
MerkleTreeHookConfig,
MultisigConfig,
Expand Down Expand Up @@ -36,11 +38,26 @@ export const core: ChainMap<CoreConfig> = objMap(
.map((origin) => [origin, defaultMultisigConfigs[origin]]),
);

const merkleRoot = (multisig: MultisigConfig): MultisigIsmConfig =>
multisigConfigToIsmConfig(IsmType.MERKLE_ROOT_MULTISIG, multisig);
const isZksyncChain =
getChain(local).technicalStack === ChainTechnicalStack.ZkSync;

// zkSync uses a different ISM for the merkle root
const merkleRoot = (multisig: MultisigConfig): MultisigIsmConfig =>
multisigConfigToIsmConfig(
isZksyncChain
? IsmType.STORAGE_MERKLE_ROOT_MULTISIG
: IsmType.MERKLE_ROOT_MULTISIG,
multisig,
);

// zkSync uses a different ISM for the message ID
const messageIdIsm = (multisig: MultisigConfig): MultisigIsmConfig =>
multisigConfigToIsmConfig(IsmType.MESSAGE_ID_MULTISIG, multisig);
multisigConfigToIsmConfig(
isZksyncChain
? IsmType.STORAGE_MESSAGE_ID_MULTISIG
: IsmType.MESSAGE_ID_MULTISIG,
multisig,
);

const routingIsm: RoutingIsmConfig = {
type: IsmType.ROUTING,
Expand All @@ -55,17 +72,30 @@ export const core: ChainMap<CoreConfig> = objMap(
...ownerConfig,
};

// No static aggregation ISM support on zkSync
const defaultZkSyncIsm = (): RoutingIsmConfig => ({
type: IsmType.ROUTING,
domains: objMap(
originMultisigs,
(_, multisig): MultisigIsmConfig => messageIdIsm(multisig),
),
...ownerConfig,
});

const pausableIsm: PausableIsmConfig = {
type: IsmType.PAUSABLE,
paused: false,
...ownerConfig,
};

const defaultIsm: AggregationIsmConfig = {
type: IsmType.AGGREGATION,
modules: [routingIsm, pausableIsm],
threshold: 2,
};
// No static aggregation ISM support on zkSync
const defaultIsm: AggregationIsmConfig | RoutingIsmConfig = isZksyncChain
? defaultZkSyncIsm()
: {
type: IsmType.AGGREGATION,
modules: [routingIsm, pausableIsm],
threshold: 2,
};

const merkleHook: MerkleTreeHookConfig = {
type: HookType.MERKLE_TREE,
Expand All @@ -79,28 +109,44 @@ export const core: ChainMap<CoreConfig> = objMap(
...ownerConfig,
};

const aggregationHooks = objMap(
// No static aggregation hook support on zkSync
const defaultHookDomains = objMap(
originMultisigs,
(_origin, _): AggregationHookConfig => ({
type: HookType.AGGREGATION,
hooks: [pausableHook, merkleHook, igpHook],
}),
(_origin, _): AggregationHookConfig | IgpConfig => {
return isZksyncChain
? igpHook
: {
type: HookType.AGGREGATION,
hooks: [pausableHook, merkleHook, igpHook],
};
},
);

const defaultHook: FallbackRoutingHookConfig = {
type: HookType.FALLBACK_ROUTING,
...ownerConfig,
domains: aggregationHooks,
domains: defaultHookDomains,
fallback: merkleHook,
};

const requiredHook: ProtocolFeeHookConfig = {
type: HookType.PROTOCOL_FEE,
maxProtocolFee: ethers.utils.parseUnits('1', 'gwei').toString(), // 1 gwei of native token
protocolFee: BigNumber.from(1).toString(), // 1 wei of native token
beneficiary: ownerConfig.owner as Address,
...ownerConfig,
};
if (typeof ownerConfig.owner !== 'string') {
throw new Error('beneficiary must be a string');
}

// No aggregation hook support on zkSync, so we ignore protocolFee
// and make the merkleTreeHook required
const requiredHook: ProtocolFeeHookConfig | MerkleTreeHookConfig =
isZksyncChain
? {
type: HookType.MERKLE_TREE,
}
: {
type: HookType.PROTOCOL_FEE,
maxProtocolFee: ethers.utils.parseUnits('1', 'gwei').toString(), // 1 gwei of native token
protocolFee: BigNumber.from(1).toString(), // 1 wei of native token
beneficiary: ownerConfig.owner as Address,
...ownerConfig,
};

return {
defaultIsm,
Expand Down
25 changes: 23 additions & 2 deletions typescript/sdk/src/ism/EvmIsmReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {

import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js';
import { DispatchedMessage } from '../core/types.js';
import { ChainTechnicalStack } from '../metadata/chainMetadataTypes.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { ChainNameOrId } from '../types.js';
import { HyperlaneReader } from '../utils/HyperlaneReader.js';
Expand Down Expand Up @@ -62,6 +63,7 @@ export interface IsmReader {

export class EvmIsmReader extends HyperlaneReader implements IsmReader {
protected readonly logger = rootLogger.child({ module: 'EvmIsmReader' });
protected isZkSyncChain: boolean;

constructor(
protected readonly multiProvider: MultiProvider,
Expand All @@ -72,6 +74,12 @@ export class EvmIsmReader extends HyperlaneReader implements IsmReader {
protected readonly messageContext?: DispatchedMessage,
) {
super(multiProvider, chain);

// So we can distinguish between Storage/Static ISMs
const chainTechnicalStack = this.multiProvider.getChainMetadata(
this.chain,
).technicalStack;
this.isZkSyncChain = chainTechnicalStack === ChainTechnicalStack.ZkSync;
}

async deriveIsmConfig(address: Address): Promise<DerivedIsmConfig> {
Expand Down Expand Up @@ -208,9 +216,14 @@ export class EvmIsmReader extends HyperlaneReader implements IsmReader {
async (module) => this.deriveIsmConfig(module),
);

// If it's a zkSync chain, it must be a StorageAggregationIsm
const ismType = this.isZkSyncChain
? IsmType.STORAGE_AGGREGATION
: IsmType.AGGREGATION;

return {
address,
type: IsmType.AGGREGATION,
type: ismType,
modules: ismConfigs,
threshold,
};
Expand All @@ -227,11 +240,19 @@ export class EvmIsmReader extends HyperlaneReader implements IsmReader {
`expected module type to be ${ModuleType.MERKLE_ROOT_MULTISIG} or ${ModuleType.MESSAGE_ID_MULTISIG}, got ${moduleType}`,
);

const ismType =
let ismType =
moduleType === ModuleType.MERKLE_ROOT_MULTISIG
? IsmType.MERKLE_ROOT_MULTISIG
: IsmType.MESSAGE_ID_MULTISIG;

// If it's a zkSync chain, it must be a StorageMultisigIsm
if (this.isZkSyncChain) {
ismType =
moduleType === ModuleType.MERKLE_ROOT_MULTISIG
? IsmType.STORAGE_MERKLE_ROOT_MULTISIG
: IsmType.STORAGE_MESSAGE_ID_MULTISIG;
}

const [validators, threshold] = await ism.validatorsAndThreshold(
ethers.constants.AddressZero,
);
Expand Down

0 comments on commit 79c61c8

Please sign in to comment.