@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
171 lines • 8.8 kB
JavaScript
import { ethers, utils as ethersUtils } from 'ethers';
import { Ownable__factory } from '@hyperlane-xyz/core';
import { assert, eqAddress, rootLogger } from '@hyperlane-xyz/utils';
import { BytecodeHash } from '../consts/bytecode.js';
import { HyperlaneAppChecker } from '../deploy/HyperlaneAppChecker.js';
import { proxyImplementation } from '../deploy/proxy.js';
import { ViolationType } from '../deploy/types.js';
import { EvmIsmReader } from '../ism/EvmIsmReader.js';
import { collectValidators, moduleMatchesConfig } from '../ism/utils.js';
import { CoreViolationType, MailboxViolationType, } from './types.js';
export class HyperlaneCoreChecker extends HyperlaneAppChecker {
ismFactory;
chainAddresses;
constructor(multiProvider, app, configMap, ismFactory, chainAddresses) {
super(multiProvider, app, configMap);
this.ismFactory = ismFactory;
this.chainAddresses = chainAddresses;
}
async checkChain(chain) {
const config = this.configMap[chain];
if (!config) {
rootLogger.warn(`No config for chain ${chain}`);
return;
}
// skip chains that are configured to be removed
if (config.remove) {
return;
}
await this.checkProxiedContracts(chain, config.owner, config.ownerOverrides);
await this.checkMailbox(chain);
await this.checkBytecodes(chain);
await this.checkValidatorAnnounce(chain);
if (config.upgrade) {
await this.checkUpgrade(chain, config.upgrade);
}
await this.checkDomainOwnership(chain);
}
async checkDomainOwnership(chain) {
const config = this.configMap[chain];
return this.checkOwnership(chain, config.owner, config.ownerOverrides);
}
async checkHook(chain, hookName, hookAddress, expectedHookOwner) {
const hook = Ownable__factory.connect(hookAddress, this.multiProvider.getProvider(chain));
const hookOwner = await hook.owner();
if (!eqAddress(hookOwner, expectedHookOwner)) {
const violation = {
type: ViolationType.Owner,
chain,
name: hookName,
actual: hookOwner,
expected: expectedHookOwner,
contract: hook,
};
this.addViolation(violation);
}
}
async checkMailbox(chain) {
const contracts = this.app.getContracts(chain);
const mailbox = contracts.mailbox;
const localDomain = await mailbox.localDomain();
assert(localDomain === this.multiProvider.getDomainId(chain), `local domain ${localDomain} does not match expected domain ${this.multiProvider.getDomainId(chain)} for ${chain}`);
const config = this.configMap[chain];
const expectedHookOwner = this.getOwner(config.owner, 'fallbackRoutingHook', config.ownerOverrides);
await this.checkHook(chain, 'defaultHook', await mailbox.defaultHook(), expectedHookOwner);
await this.checkHook(chain, 'requiredHook', await mailbox.requiredHook(), expectedHookOwner);
const actualIsmAddress = await mailbox.defaultIsm();
const matches = await moduleMatchesConfig(chain, actualIsmAddress, config.defaultIsm, this.ismFactory.multiProvider, this.ismFactory.getContracts(chain));
if (!matches) {
const registryIsmAddress = this.chainAddresses[chain].interchainSecurityModule;
const registryIsmMatches = await moduleMatchesConfig(chain, registryIsmAddress, config.defaultIsm, this.ismFactory.multiProvider, this.ismFactory.getContracts(chain));
if (registryIsmMatches) {
// if the ISM in registry matches the expected config, then we can assume
// that the mailbox should be using that ISM instead of the current one
// and we should report just an address violation
const violation = {
type: CoreViolationType.Mailbox,
subType: MailboxViolationType.DefaultIsm,
contract: mailbox,
chain,
actual: actualIsmAddress,
expected: registryIsmAddress,
};
this.addViolation(violation);
}
else {
const ismReader = new EvmIsmReader(this.ismFactory.multiProvider, chain);
let actualConfig = ethers.constants.AddressZero;
if (actualIsmAddress !== ethers.constants.AddressZero) {
actualConfig = await ismReader.deriveIsmConfig(actualIsmAddress);
}
// If the config doesn't match either onchain or the registry
// then we have a full config violation, which the governor will need to
// fix by deploying a new ISM
const violation = {
type: CoreViolationType.Mailbox,
subType: MailboxViolationType.DefaultIsm,
contract: mailbox,
chain,
actual: actualConfig,
expected: config.defaultIsm,
};
this.addViolation(violation);
}
}
}
async checkBytecodes(chain) {
const contracts = this.app.getContracts(chain);
const mailbox = contracts.mailbox;
const localDomain = await mailbox.localDomain();
const implementation = await proxyImplementation(this.multiProvider.getProvider(chain), mailbox.address);
if (implementation === ethers.constants.AddressZero) {
const violation = {
type: CoreViolationType.Mailbox,
subType: MailboxViolationType.NotProxied,
contract: mailbox,
chain,
actual: implementation,
expected: 'non-zero address',
};
this.addViolation(violation);
}
else {
await this.checkBytecode(chain, 'Mailbox implementation', implementation, [
BytecodeHash.V3_MAILBOX_BYTECODE_HASH,
BytecodeHash.OPT_V3_MAILBOX_BYTECODE_HASH,
], (bytecode) =>
// This is obviously super janky but basically we are searching
// for the occurrences of localDomain in the bytecode and remove
// that to compare, but some coincidental occurrences of
// localDomain in the bytecode should be not be removed which
// are just done via an offset guard
bytecode
.replaceAll(ethersUtils.defaultAbiCoder
.encode(['uint32'], [localDomain])
.slice(2), (match, offset) => (offset > 8000 ? match : ''))
// We persist the block number in the bytecode now too, so we have to strip it
.replaceAll(/(00000000000000000000000000000000000000000000000000000000[a-f0-9]{0,22})81565/g, (match, _offset) => (match.length % 2 === 0 ? '' : '0'))
.replaceAll(/(0000000000000000000000000000000000000000000000000000[a-f0-9]{0,22})6118123373/g, (match, _offset) => (match.length % 2 === 0 ? '' : '0'))
.replaceAll(/(f167f00000000000000000000000000000000000000000000000000000[a-f0-9]{0,22})338989898/g, (match, _offset) => (match.length % 2 === 0 ? '' : '0')));
}
await this.checkProxy(chain, 'Mailbox proxy', contracts.mailbox.address);
await this.checkBytecode(chain, 'ProxyAdmin', contracts.proxyAdmin.address, [
BytecodeHash.PROXY_ADMIN_BYTECODE_HASH,
BytecodeHash.V2_PROXY_ADMIN_BYTECODE_HASH,
]);
}
async checkValidatorAnnounce(chain) {
const validators = new Set();
const remotes = Object.keys(this.configMap).filter((c) => c !== chain);
const remoteOriginValidators = remotes.map((remote) => collectValidators(chain, this.configMap[remote].defaultIsm));
remoteOriginValidators.map((set) => {
[...set].map((v) => validators.add(v));
});
const validatorAnnounce = this.app.getContracts(chain).validatorAnnounce;
const announcedValidators = await validatorAnnounce.getAnnouncedValidators();
[...validators].forEach((validator) => {
const matches = announcedValidators.filter((x) => eqAddress(x, validator));
if (matches.length == 0) {
const violation = {
type: CoreViolationType.ValidatorAnnounce,
chain,
validator,
actual: false,
expected: true,
};
this.addViolation(violation);
}
});
}
}
//# sourceMappingURL=HyperlaneCoreChecker.js.map