Pausable ISM (#3141)

### Description

- Adds pausable hook and ism config support to deployers
- Configure testnet pausable hook and ISM
- Add pausable ism checking

### Drive-by changes

- Refactors ownable config

### Related issues

- https://github.com/hyperlane-xyz/issues/issues/706

### Backward compatibility

- Yes

### Testing

- Unit Tests and fork tests
pull/3163/head
Yorke Rhodes 10 months ago committed by GitHub
parent 3c298d0646
commit 0727a6178e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      solidity/contracts/isms/PausableIsm.sol
  2. 13
      solidity/test/isms/PausableIsm.t.sol
  3. 59
      typescript/infra/config/environments/mainnet3/core.ts
  4. 29
      typescript/infra/config/environments/testnet4/core.ts
  5. 4
      typescript/infra/src/deployment/deploy.ts
  6. 8
      typescript/sdk/src/core/types.ts
  7. 2
      typescript/sdk/src/deploy/HyperlaneDeployer.ts
  8. 10
      typescript/sdk/src/deploy/types.ts
  9. 7
      typescript/sdk/src/gas/types.ts
  10. 21
      typescript/sdk/src/hook/HyperlaneHookDeployer.ts
  11. 7
      typescript/sdk/src/hook/contracts.ts
  12. 15
      typescript/sdk/src/hook/types.ts
  13. 4
      typescript/sdk/src/index.ts
  14. 37
      typescript/sdk/src/ism/HyperlaneIsmFactory.ts
  15. 17
      typescript/sdk/src/ism/types.ts
  16. 3
      typescript/sdk/src/router/HyperlaneRouterChecker.ts
  17. 6
      typescript/sdk/src/router/types.ts

@ -11,6 +11,10 @@ import {IInterchainSecurityModule} from "../interfaces/IInterchainSecurityModule
contract PausableIsm is IInterchainSecurityModule, Ownable, Pausable {
uint8 public constant override moduleType = uint8(Types.NULL);
constructor(address owner) Ownable() Pausable() {
_transferOwnership(owner);
}
/**
* @inheritdoc IInterchainSecurityModule
* @dev Reverts when paused, otherwise returns `true`.

@ -8,24 +8,35 @@ import {PausableIsm} from "../../contracts/isms/PausableIsm.sol";
contract PausableIsmTest is Test {
PausableIsm ism;
address owner;
function setUp() public {
ism = new PausableIsm();
owner = msg.sender;
ism = new PausableIsm(owner);
}
function test_verify() public {
assertTrue(ism.verify("", ""));
vm.prank(owner);
ism.pause();
vm.expectRevert(bytes("Pausable: paused"));
ism.verify("", "");
}
function test_pause() public {
vm.expectRevert(bytes("Ownable: caller is not the owner"));
ism.pause();
vm.prank(owner);
ism.pause();
assertTrue(ism.paused());
}
function test_unpause() public {
vm.prank(owner);
ism.pause();
vm.expectRevert(bytes("Ownable: caller is not the owner"));
ism.unpause();
vm.prank(owner);
ism.unpause();
assertFalse(ism.paused());
}

@ -2,23 +2,67 @@ import { BigNumber, ethers } from 'ethers';
import {
AggregationHookConfig,
AggregationIsmConfig,
ChainMap,
CoreConfig,
HookType,
IgpHookConfig,
IsmType,
MerkleTreeHookConfig,
MultisigConfig,
MultisigIsmConfig,
PausableHookConfig,
PausableIsmConfig,
ProtocolFeeHookConfig,
RoutingIsmConfig,
defaultMultisigConfigs,
} from '@hyperlane-xyz/sdk';
import { objMap } from '@hyperlane-xyz/utils';
import { Contexts } from '../../contexts';
import { routingIsm } from '../../routingIsm';
import { supportedChainNames } from './chains';
import { igp } from './igp';
import { owners, safes } from './owners';
export const core: ChainMap<CoreConfig> = objMap(owners, (local, owner) => {
const defaultIsm = routingIsm('mainnet3', local, Contexts.Hyperlane);
const originMultisigs: ChainMap<MultisigConfig> = Object.fromEntries(
supportedChainNames
.filter((chain) => chain !== local)
.map((origin) => [origin, defaultMultisigConfigs[origin]]),
);
const merkleRoot = (multisig: MultisigConfig): MultisigIsmConfig => ({
type: IsmType.MERKLE_ROOT_MULTISIG,
...multisig,
});
const messageIdIsm = (multisig: MultisigConfig): MultisigIsmConfig => ({
type: IsmType.MESSAGE_ID_MULTISIG,
...multisig,
});
const routingIsm: RoutingIsmConfig = {
type: IsmType.ROUTING,
domains: objMap(
originMultisigs,
(_, multisig): AggregationIsmConfig => ({
type: IsmType.AGGREGATION,
modules: [messageIdIsm(multisig), merkleRoot(multisig)],
threshold: 1,
}),
),
owner,
};
const pausableIsm: PausableIsmConfig = {
type: IsmType.PAUSABLE,
owner,
};
const defaultIsm: AggregationIsmConfig = {
type: IsmType.AGGREGATION,
modules: [routingIsm, pausableIsm],
threshold: 2,
};
const merkleHook: MerkleTreeHookConfig = {
type: HookType.MERKLE_TREE,
@ -29,9 +73,14 @@ export const core: ChainMap<CoreConfig> = objMap(owners, (local, owner) => {
...igp[local],
};
const pausableHook: PausableHookConfig = {
type: HookType.PAUSABLE,
owner,
};
const defaultHook: AggregationHookConfig = {
type: HookType.AGGREGATION,
hooks: [merkleHook, igpHook],
hooks: [pausableHook, merkleHook, igpHook],
};
const requiredHook: ProtocolFeeHookConfig = {

@ -5,17 +5,19 @@ import {
AggregationIsmConfig,
ChainMap,
CoreConfig,
FallbackRoutingHookConfig,
HookType,
IgpHookConfig,
IsmType,
MerkleTreeHookConfig,
MultisigConfig,
MultisigIsmConfig,
PausableHookConfig,
PausableIsmConfig,
ProtocolFeeHookConfig,
RoutingIsmConfig,
defaultMultisigConfigs,
} from '@hyperlane-xyz/sdk';
import { DomainRoutingHookConfig } from '@hyperlane-xyz/sdk/src/hook/types';
import { objMap } from '@hyperlane-xyz/utils';
import { supportedChainNames } from './chains';
@ -39,7 +41,7 @@ export const core: ChainMap<CoreConfig> = objMap(owners, (local, owner) => {
...multisig,
});
const defaultIsm: RoutingIsmConfig = {
const routingIsm: RoutingIsmConfig = {
type: IsmType.ROUTING,
domains: objMap(
originMultisigs,
@ -52,6 +54,17 @@ export const core: ChainMap<CoreConfig> = objMap(owners, (local, owner) => {
owner,
};
const pausableIsm: PausableIsmConfig = {
type: IsmType.PAUSABLE,
owner,
};
const defaultIsm: AggregationIsmConfig = {
type: IsmType.AGGREGATION,
modules: [routingIsm, pausableIsm],
threshold: 2,
};
const merkleHook: MerkleTreeHookConfig = {
type: HookType.MERKLE_TREE,
};
@ -61,18 +74,22 @@ export const core: ChainMap<CoreConfig> = objMap(owners, (local, owner) => {
...igp[local],
};
const pausableHook: PausableHookConfig = {
type: HookType.PAUSABLE,
owner,
};
const aggregationHooks = objMap(
originMultisigs,
(_origin, _): AggregationHookConfig => ({
type: HookType.AGGREGATION,
hooks: [igpHook, merkleHook],
hooks: [pausableHook, merkleHook, igpHook],
}),
);
const defaultHook: FallbackRoutingHookConfig = {
type: HookType.FALLBACK_ROUTING,
const defaultHook: DomainRoutingHookConfig = {
type: HookType.ROUTING,
owner,
fallback: merkleHook,
domains: aggregationHooks,
};

@ -20,7 +20,7 @@ import {
writeMergedJSONAtPath,
} from '../utils/utils';
export async function deployWithArtifacts<Config>(
export async function deployWithArtifacts<Config extends object>(
configMap: ChainMap<Config>,
deployer: HyperlaneDeployer<Config, any>,
cache: {
@ -71,7 +71,7 @@ export async function deployWithArtifacts<Config>(
await postDeploy(deployer, cache, agentConfig);
}
export async function postDeploy<Config>(
export async function postDeploy<Config extends object>(
deployer: HyperlaneDeployer<Config, any>,
cache: {
addresses: string;

@ -2,17 +2,17 @@ import type { Mailbox } from '@hyperlane-xyz/core';
import type { Address, ParsedMessage } from '@hyperlane-xyz/utils';
import type { UpgradeConfig } from '../deploy/proxy';
import type { CheckerViolation } from '../deploy/types';
import type { CheckerViolation, OwnableConfig } from '../deploy/types';
import { HookConfig } from '../hook/types';
import type { IsmConfig } from '../ism/types';
import type { ChainName } from '../types';
export type CoreConfig = {
import { CoreFactories } from './contracts';
export type CoreConfig = OwnableConfig<keyof CoreFactories> & {
defaultIsm: IsmConfig;
defaultHook: HookConfig;
requiredHook: HookConfig;
owner: Address;
ownerOverrides?: Record<string, string>;
remove?: boolean;
upgrade?: UpgradeConfig;
};

@ -53,7 +53,7 @@ export interface DeployerOptions {
}
export abstract class HyperlaneDeployer<
Config,
Config extends object,
Factories extends HyperlaneFactories,
> {
public verificationInputs: ChainMap<ContractVerificationInput[]> = {};

@ -5,9 +5,19 @@ import type {
Ownable,
TimelockController,
} from '@hyperlane-xyz/core';
import { Address } from '@hyperlane-xyz/utils';
import type { ChainName } from '../types';
export type OwnableConfig<Keys extends string = string> = {
owner: Address;
ownerOverrides?: Partial<Record<Keys, Address>>;
};
export function isOwnableConfig(config: object): config is OwnableConfig {
return 'owner' in config;
}
export interface CheckerViolation {
chain: ChainName;
type: string;

@ -3,15 +3,16 @@ import { BigNumber } from 'ethers';
import { InterchainGasPaymaster } from '@hyperlane-xyz/core';
import type { Address } from '@hyperlane-xyz/utils';
import type { CheckerViolation } from '../deploy/types';
import type { CheckerViolation, OwnableConfig } from '../deploy/types';
import { ChainMap } from '../types';
import { IgpFactories } from './contracts';
export enum GasOracleContractType {
StorageGasOracle = 'StorageGasOracle',
}
export type IgpConfig = {
owner: Address;
export type IgpConfig = OwnableConfig<keyof IgpFactories> & {
beneficiary: Address;
gasOracleType: ChainMap<GasOracleContractType>;
oracleKey: Address;

@ -22,7 +22,7 @@ import { IsmType, OpStackIsmConfig } from '../ism/types';
import { MultiProvider } from '../providers/MultiProvider';
import { ChainMap, ChainName } from '../types';
import { HookFactories, hookFactories } from './contracts';
import { DeployedHook, HookFactories, hookFactories } from './contracts';
import {
AggregationHookConfig,
DomainRoutingHookConfig,
@ -59,17 +59,20 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer<
config: HookConfig,
coreAddresses = this.core[chain],
): Promise<HyperlaneContracts<HookFactories>> {
// other simple hooks can go here
let hook;
let hook: DeployedHook;
if (config.type === HookType.MERKLE_TREE) {
const mailbox = coreAddresses.mailbox;
if (!mailbox) {
throw new Error(`Mailbox address is required for ${config.type}`);
}
hook = await this.deployContract(chain, config.type, [mailbox]);
return { [config.type]: hook } as any;
} else if (config.type === HookType.INTERCHAIN_GAS_PAYMASTER) {
return this.deployIgp(chain, config, coreAddresses) as any;
const { interchainGasPaymaster } = await this.deployIgp(
chain,
config,
coreAddresses,
);
hook = interchainGasPaymaster;
} else if (config.type === HookType.AGGREGATION) {
return this.deployAggregation(chain, config, coreAddresses); // deploy from factory
} else if (config.type === HookType.PROTOCOL_FEE) {
@ -81,8 +84,14 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer<
config.type === HookType.FALLBACK_ROUTING
) {
hook = await this.deployRouting(chain, config, coreAddresses);
} else if (config.type === HookType.PAUSABLE) {
hook = await this.deployContract(chain, config.type, []);
await this.transferOwnershipOfContracts(chain, config.owner, { hook });
} else {
throw new Error(`Unsupported hook config: ${config}`);
}
const deployedContracts = { [config.type]: hook } as any;
const deployedContracts = { [config.type]: hook } as any; // partial
this.addDeployedContracts(chain, deployedContracts);
return deployedContracts;
}

@ -4,9 +4,11 @@ import {
InterchainGasPaymaster__factory,
MerkleTreeHook__factory,
OPStackHook__factory,
PausableHook__factory,
ProtocolFee__factory,
StaticAggregationHook__factory,
} from '@hyperlane-xyz/core';
import { ValueOf } from '@hyperlane-xyz/utils';
import { HookType } from './types';
@ -18,6 +20,11 @@ export const hookFactories = {
[HookType.OP_STACK]: new OPStackHook__factory(),
[HookType.ROUTING]: new DomainRoutingHook__factory(),
[HookType.FALLBACK_ROUTING]: new FallbackDomainRoutingHook__factory(),
[HookType.PAUSABLE]: new PausableHook__factory(),
};
export type HookFactories = typeof hookFactories;
export type DeployedHook = Awaited<
ReturnType<ValueOf<HookFactories>['deploy']>
>;

@ -1,5 +1,6 @@
import { Address } from '@hyperlane-xyz/utils';
import { OwnableConfig } from '../deploy/types';
import { IgpConfig } from '../gas/types';
import { ChainMap, ChainName } from '../types';
@ -11,6 +12,7 @@ export enum HookType {
OP_STACK = 'opStackHook',
ROUTING = 'domainRoutingHook',
FALLBACK_ROUTING = 'fallbackRoutingHook',
PAUSABLE = 'pausableHook',
}
export type MerkleTreeHookConfig = {
@ -26,12 +28,15 @@ export type IgpHookConfig = IgpConfig & {
type: HookType.INTERCHAIN_GAS_PAYMASTER;
};
export type ProtocolFeeHookConfig = {
export type ProtocolFeeHookConfig = OwnableConfig & {
type: HookType.PROTOCOL_FEE;
maxProtocolFee: string;
protocolFee: string;
beneficiary: Address;
owner: Address;
};
export type PausableHookConfig = OwnableConfig & {
type: HookType.PAUSABLE;
};
export type OpStackHookConfig = {
@ -40,8 +45,7 @@ export type OpStackHookConfig = {
destinationChain: ChainName;
};
type RoutingHookConfig = {
owner: Address;
type RoutingHookConfig = OwnableConfig & {
domains: ChainMap<HookConfig>;
};
@ -61,7 +65,8 @@ export type HookConfig =
| ProtocolFeeHookConfig
| OpStackHookConfig
| DomainRoutingHookConfig
| FallbackRoutingHookConfig;
| FallbackRoutingHookConfig
| PausableHookConfig;
export type HooksConfig = {
required: HookConfig;

@ -82,6 +82,7 @@ export { DeployerOptions, HyperlaneDeployer } from './deploy/HyperlaneDeployer';
export { HyperlaneProxyFactoryDeployer } from './deploy/HyperlaneProxyFactoryDeployer';
export {
CheckerViolation,
OwnableConfig,
OwnerViolation,
ViolationType,
} from './deploy/types';
@ -125,6 +126,7 @@ export {
IgpHookConfig,
MerkleTreeHookConfig,
OpStackHookConfig,
PausableHookConfig,
ProtocolFeeHookConfig,
} from './hook/types';
export {
@ -145,6 +147,7 @@ export {
MultisigConfig,
MultisigIsmConfig,
OpStackIsmConfig,
PausableIsmConfig,
RoutingIsmConfig,
} from './ism/types';
export {
@ -303,7 +306,6 @@ export {
GasConfig,
GasRouterConfig,
MailboxClientConfig,
OwnableConfig,
ProxiedFactories,
ProxiedRouterConfig,
RouterAddress,

@ -16,6 +16,7 @@ import {
MailboxClient__factory,
OPStackIsm,
OPStackIsm__factory,
PausableIsm__factory,
StaticAddressSetFactory,
StaticAggregationIsm__factory,
StaticThresholdAddressSetFactory,
@ -145,6 +146,13 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
case IsmType.OP_STACK:
contract = await this.deployOpStackIsm(destination, config);
break;
case IsmType.PAUSABLE:
contract = await this.multiProvider.handleDeploy(
destination,
new PausableIsm__factory(),
[config.owner],
);
break;
case IsmType.TEST_ISM:
contract = await this.multiProvider.handleDeploy(
destination,
@ -616,7 +624,7 @@ export async function moduleMatchesConfig(
);
// Check that the RoutingISM owner matches the config
const owner = await routingIsm.owner();
matches = matches && eqAddress(owner, config.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(moduleAddress, provider);
@ -653,8 +661,8 @@ export async function moduleMatchesConfig(
const [subModules, threshold] = await aggregationIsm.modulesAndThreshold(
'0x',
);
matches = matches && threshold === config.threshold;
matches = matches && subModules.length === config.modules.length;
matches &&= threshold === config.threshold;
matches &&= subModules.length === config.modules.length;
const configIndexMatched = new Map();
for (const subModule of subModules) {
@ -666,12 +674,12 @@ export async function moduleMatchesConfig(
// The submodule returned by the ISM must match exactly one
// entry in the config.
const count = subModuleMatchesConfig.filter(Boolean).length;
matches = matches && count === 1;
matches &&= count === 1;
// That entry in the config should not have been matched already.
subModuleMatchesConfig.forEach((matched, index) => {
if (matched) {
matches = matches && !configIndexMatched.has(index);
matches &&= !configIndexMatched.has(index);
configIndexMatched.set(index, true);
}
});
@ -681,7 +689,7 @@ export async function moduleMatchesConfig(
case IsmType.OP_STACK: {
const opStackIsm = OPStackIsm__factory.connect(moduleAddress, provider);
const type = await opStackIsm.moduleType();
matches = matches && type === ModuleType.NULL;
matches &&= type === ModuleType.NULL;
break;
}
case IsmType.TEST_ISM: {
@ -689,6 +697,17 @@ export async function moduleMatchesConfig(
matches = true;
break;
}
case IsmType.PAUSABLE: {
const pausableIsm = PausableIsm__factory.connect(moduleAddress, 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');
}
@ -789,8 +808,10 @@ export function collectValidators(
aggregatedValidators.forEach((set) => {
validators = validators.concat([...set]);
});
} else if (config.type === IsmType.TEST_ISM) {
// This is just a TestISM
} else if (
config.type === IsmType.TEST_ISM ||
config.type === IsmType.PAUSABLE
) {
return new Set([]);
} else {
throw new Error('Unsupported ModuleType');

@ -3,10 +3,12 @@ import {
IMultisigIsm,
IRoutingIsm,
OPStackIsm,
PausableIsm,
TestIsm,
} from '@hyperlane-xyz/core';
import type { Address, Domain, ValueOf } from '@hyperlane-xyz/utils';
import { OwnableConfig } from '../deploy/types';
import { ChainMap } from '../types';
// this enum should match the IInterchainSecurityModule.sol enum
@ -31,6 +33,7 @@ export enum IsmType {
MERKLE_ROOT_MULTISIG = 'merkleRootMultisigIsm',
MESSAGE_ID_MULTISIG = 'messageIdMultisigIsm',
TEST_ISM = 'testIsm',
PAUSABLE = 'pausableIsm',
}
// mapping between the two enums
@ -50,6 +53,8 @@ export function ismTypeToModuleType(ismType: IsmType): ModuleType {
return ModuleType.MESSAGE_ID_MULTISIG;
case IsmType.TEST_ISM:
return ModuleType.NULL;
case IsmType.PAUSABLE:
return ModuleType.NULL;
}
}
@ -66,9 +71,13 @@ export type TestIsmConfig = {
type: IsmType.TEST_ISM;
};
export type RoutingIsmConfig = {
export type PausableIsmConfig = OwnableConfig & {
type: IsmType.PAUSABLE;
paused?: boolean;
};
export type RoutingIsmConfig = OwnableConfig & {
type: IsmType.ROUTING | IsmType.FALLBACK_ROUTING;
owner: Address;
domains: ChainMap<IsmConfig>;
};
@ -90,7 +99,8 @@ export type IsmConfig =
| MultisigIsmConfig
| AggregationIsmConfig
| OpStackIsmConfig
| TestIsmConfig;
| TestIsmConfig
| PausableIsmConfig;
export type DeployedIsmType = {
[IsmType.ROUTING]: IRoutingIsm;
@ -100,6 +110,7 @@ export type DeployedIsmType = {
[IsmType.MESSAGE_ID_MULTISIG]: IMultisigIsm;
[IsmType.OP_STACK]: OPStackIsm;
[IsmType.TEST_ISM]: TestIsm;
[IsmType.PAUSABLE]: PausableIsm;
};
export type DeployedIsm = ValueOf<DeployedIsmType>;

@ -19,7 +19,6 @@ import {
ClientViolation,
ClientViolationType,
MailboxClientConfig,
OwnableConfig,
RouterConfig,
RouterViolation,
RouterViolationType,
@ -49,7 +48,7 @@ export class HyperlaneRouterChecker<
const router = this.app.router(this.app.getContracts(chain));
const checkMailboxClientProperty = async (
property: keyof (MailboxClientConfig & OwnableConfig),
property: keyof MailboxClientConfig,
actual: string,
violationType: ClientViolationType,
) => {

@ -8,17 +8,13 @@ import type { Address } from '@hyperlane-xyz/utils';
import { HyperlaneFactories } from '../contracts/types';
import { UpgradeConfig } from '../deploy/proxy';
import { CheckerViolation } from '../deploy/types';
import { CheckerViolation, OwnableConfig } from '../deploy/types';
import { IsmConfig } from '../ism/types';
export type RouterAddress = {
router: Address;
};
export type OwnableConfig = {
owner: Address;
};
export type ForeignDeploymentConfig = {
foreignDeployment?: Address;
};

Loading…
Cancel
Save