@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
222 lines • 10.3 kB
JavaScript
import { utils } from 'ethers';
import { ProxyAdmin__factory, TimelockController__factory, } from '@hyperlane-xyz/core';
import { ProtocolType, assert, eqAddress, isZeroishAddress, objFilter, objMap, promiseObjAll, rootLogger, } from '@hyperlane-xyz/utils';
import { BytecodeHash } from '../consts/bytecode.js';
import { filterOwnableContracts } from '../contracts/contracts.js';
import { isProxy, proxyAdmin } from './proxy.js';
import { ViolationType, } from './types.js';
export class HyperlaneAppChecker {
multiProvider;
app;
configMap;
violations = [];
constructor(multiProvider, app, configMap) {
this.multiProvider = multiProvider;
this.app = app;
this.configMap = configMap;
}
async check(chainsToCheck) {
// Get all EVM chains from config
const evmChains = this.getEvmChains();
// Mark any EVM chains that are not deployed
const appChains = this.app.chains();
for (const chain of evmChains) {
if (!appChains.includes(chain)) {
this.addViolation({
type: ViolationType.NotDeployed,
chain,
expected: '',
actual: '',
});
}
}
// Finally, check the chains that were explicitly requested
// If no chains were requested, check all app chains
const chains = !chainsToCheck || chainsToCheck.length === 0 ? appChains : chainsToCheck;
return Promise.all(chains
.filter((chain) => this.multiProvider.getChainMetadata(chain).protocol ===
ProtocolType.Ethereum)
.map((chain) => this.checkChain(chain)));
}
getEvmChains() {
return Object.keys(this.configMap).filter((chain) => this.multiProvider.getChainMetadata(chain).protocol ===
ProtocolType.Ethereum);
}
addViolation(violation) {
if (violation.type === ViolationType.BytecodeMismatch) {
rootLogger.warn({ violation }, `Found bytecode mismatch. Ignoring...`);
return;
}
this.violations.push(violation);
}
async checkProxiedContracts(chain, owner, ownableOverrides) {
// expectedProxyAdminAddress may be undefined, this means that proxyAdmin is not set in the config/not known at deployment time
const expectedProxyAdminAddress = this.app.getContracts(chain).proxyAdmin?.address;
const provider = this.multiProvider.getProvider(chain);
const contracts = objFilter(this.app.getContracts(chain), (_name, contract) => !isZeroishAddress(contract.address));
await promiseObjAll(objMap(contracts, async (name, contract) => {
if (await isProxy(provider, contract.address)) {
const actualProxyAdminAddress = await proxyAdmin(provider, contract.address);
if (expectedProxyAdminAddress) {
// config defines an expected ProxyAdmin address, we therefore check if the actual ProxyAdmin address matches the expected one
if (!eqAddress(actualProxyAdminAddress, expectedProxyAdminAddress)) {
this.addViolation({
type: ViolationType.ProxyAdmin,
chain,
name,
expected: expectedProxyAdminAddress,
actual: actualProxyAdminAddress,
proxyAddress: contract.address,
});
}
}
else {
// config does not define an expected ProxyAdmin address, this means that checkOwnership will not be able to check the ownership of the ProxyAdmin contract
// as it is not explicitly defined in the config. We therefore check the ownership of the ProxyAdmin contract here.
const actualProxyAdminContract = ProxyAdmin__factory.connect(actualProxyAdminAddress, provider);
const actualProxyAdminOwner = await actualProxyAdminContract.owner();
const expectedOwner = this.getOwner(owner, 'proxyAdmin', ownableOverrides);
if (!eqAddress(actualProxyAdminOwner, expectedOwner)) {
const violation = {
chain,
name: 'proxyAdmin',
type: ViolationType.Owner,
actual: actualProxyAdminOwner,
expected: expectedOwner,
contract: actualProxyAdminContract,
};
this.addViolation(violation);
}
}
}
}));
}
async checkUpgrade(chain, upgradeConfig) {
const proxyOwner = await this.app.getContracts(chain).proxyAdmin.owner();
const timelockController = TimelockController__factory.connect(proxyOwner, this.multiProvider.getProvider(chain));
const minDelay = (await timelockController.getMinDelay()).toNumber();
if (minDelay !== upgradeConfig.timelock.delay) {
const violation = {
type: ViolationType.TimelockController,
chain,
actual: minDelay,
expected: upgradeConfig.timelock.delay,
contract: timelockController,
};
this.addViolation(violation);
}
const roleIds = {
executor: await timelockController.EXECUTOR_ROLE(),
proposer: await timelockController.PROPOSER_ROLE(),
canceller: await timelockController.CANCELLER_ROLE(),
admin: await timelockController.TIMELOCK_ADMIN_ROLE(),
};
const accountHasRole = await promiseObjAll(objMap(upgradeConfig.timelock.roles, async (role, account) => ({
hasRole: await timelockController.hasRole(roleIds[role], account),
account,
})));
for (const [role, { hasRole, account }] of Object.entries(accountHasRole)) {
if (!hasRole) {
const violation = {
type: ViolationType.AccessControl,
chain,
account,
actual: false,
expected: true,
contract: timelockController,
role,
};
this.addViolation(violation);
}
}
}
removeBytecodeMetadata(bytecode) {
// https://docs.soliditylang.org/en/v0.8.17/metadata.html#encoding-of-the-metadata-hash-in-the-bytecode
// Remove solc metadata from bytecode
return bytecode.substring(0, bytecode.length - 90);
}
getOwner(owner, contractName, ownableOverrides) {
return ownableOverrides?.[contractName] ?? owner;
}
// This method checks whether the bytecode of a contract matches the expected bytecode. It forces the deployer to explicitly acknowledge a change in bytecode. The violations can be remediated by updating the expected bytecode hash.
async checkBytecode(chain, name, address, expectedBytecodeHashes, modifyBytecodePriorToHash = (_) => _) {
const provider = this.multiProvider.getProvider(chain);
const bytecode = await provider.getCode(address);
const bytecodeHash = utils.keccak256(modifyBytecodePriorToHash(this.removeBytecodeMetadata(bytecode)));
if (!expectedBytecodeHashes.includes(bytecodeHash)) {
this.addViolation({
type: ViolationType.BytecodeMismatch,
chain,
expected: expectedBytecodeHashes,
actual: bytecodeHash,
name,
});
}
}
async checkProxy(chain, name, address) {
return this.checkBytecode(chain, name, address, [
BytecodeHash.TRANSPARENT_PROXY_BYTECODE_HASH,
BytecodeHash.TRANSPARENT_PROXY_4_9_3_BYTECODE_HASH,
BytecodeHash.OPT_TRANSPARENT_PROXY_BYTECODE_HASH,
]);
}
async ownables(chain) {
const contracts = this.app.getContracts(chain);
return filterOwnableContracts(contracts);
}
async checkOwnership(chain, owner, ownableOverrides) {
const ownableContracts = await this.ownables(chain);
for (const [name, contract] of Object.entries(ownableContracts)) {
const expectedOwner = this.getOwner(owner, name, ownableOverrides);
const actual = await contract.owner();
if (!eqAddress(actual, expectedOwner)) {
const violation = {
chain,
name,
type: ViolationType.Owner,
actual,
expected: expectedOwner,
contract,
};
this.addViolation(violation);
}
}
}
expectViolations(violationCounts) {
// Every type should have exactly the number of expected matches.
objMap(violationCounts, (type, count) => {
const actual = this.violations.filter((v) => v.type === type).length;
assert(actual == count, `Expected ${count} ${type} violations, got ${actual}`);
});
this.violations
.filter((v) => !(v.type in violationCounts))
.map((v) => {
assert(false, `Unexpected violation: ${JSON.stringify(v)}`);
});
}
expectEmpty() {
const count = this.violations.length;
assert(count === 0, `Found ${count} violations`);
}
logViolationsTable() {
const violations = this.violations;
if (violations.length > 0) {
// eslint-disable-next-line no-console
console.table(violations, [
'chain',
'remote',
'name',
'type',
'subType',
'actual',
'expected',
'description',
]);
}
else {
// eslint-disable-next-line no-console
console.info(`${module} Checker found no violations`);
}
}
}
//# sourceMappingURL=HyperlaneAppChecker.js.map