diff --git a/typescript/infra/scripts/check-deploy.ts b/typescript/infra/scripts/check-deploy.ts index 9a3e9c58e..44138ebc0 100644 --- a/typescript/infra/scripts/check-deploy.ts +++ b/typescript/infra/scripts/check-deploy.ts @@ -25,6 +25,7 @@ async function check() { console.table(coreChecker.violations, [ 'chain', 'remote', + 'name', 'type', 'subType', 'actual', diff --git a/typescript/sdk/src/deploy/HyperlaneAppChecker.ts b/typescript/sdk/src/deploy/HyperlaneAppChecker.ts index 629ae4cce..aa9acd0a9 100644 --- a/typescript/sdk/src/deploy/HyperlaneAppChecker.ts +++ b/typescript/sdk/src/deploy/HyperlaneAppChecker.ts @@ -1,3 +1,5 @@ +import { keccak256 } from 'ethers/lib/utils'; + import { Ownable } from '@hyperlane-xyz/core'; import { utils } from '@hyperlane-xyz/utils'; import type { types } from '@hyperlane-xyz/utils'; @@ -9,7 +11,12 @@ import { ChainMap, ChainName } from '../types'; import { objMap } from '../utils/objects'; import { proxyAdmin, proxyImplementation, proxyViolation } from './proxy'; -import { CheckerViolation, OwnerViolation, ViolationType } from './types'; +import { + BytecodeMismatchViolation, + CheckerViolation, + OwnerViolation, + ViolationType, +} from './types'; export abstract class HyperlaneAppChecker< Chain extends ChainName, @@ -77,6 +84,36 @@ export abstract class HyperlaneAppChecker< } } + private removeBytecodeMetadata(bytecode: string): string { + // 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); + } + + // 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: Chain, + name: string, + address: string, + expectedBytecodeHash: string, + modifyBytecodePriorToHash: (bytecode: string) => string = (_) => _, + ): Promise { + const provider = this.multiProvider.getChainProvider(chain); + const bytecode = await provider.getCode(address); + const bytecodeHash = keccak256( + modifyBytecodePriorToHash(this.removeBytecodeMetadata(bytecode)), + ); + if (bytecodeHash !== expectedBytecodeHash) { + this.addViolation({ + type: ViolationType.BytecodeMismatch, + chain, + expected: expectedBytecodeHash, + actual: bytecodeHash, + name, + } as BytecodeMismatchViolation); + } + } + async checkOwnership( chain: Chain, owner: types.Address, diff --git a/typescript/sdk/src/deploy/core/HyperlaneCoreChecker.ts b/typescript/sdk/src/deploy/core/HyperlaneCoreChecker.ts index 784542ae8..288381d54 100644 --- a/typescript/sdk/src/deploy/core/HyperlaneCoreChecker.ts +++ b/typescript/sdk/src/deploy/core/HyperlaneCoreChecker.ts @@ -1,3 +1,5 @@ +import { defaultAbiCoder } from 'ethers/lib/utils'; + import { utils } from '@hyperlane-xyz/utils'; import { eqAddress } from '@hyperlane-xyz/utils/dist/src/utils'; @@ -17,6 +19,18 @@ import { ValidatorAnnounceViolation, } from './types'; +const MAILBOX_WITHOUT_LOCAL_DOMAIN_BYTE_CODE_HASH = + '0x29b7294ab3ad2e8587e5cce0e2289ce65e12a2ea2f1e7ab34a05e7737616f457'; +const TRANSPARENT_PROXY_BYTECODE_HASH = + '0x4dde3d0906b6492bf1d4947f667afe8d53c8899f1d8788cabafd082938dceb2d'; +const MULTISIG_ISM_BYTECODE_HASH = + '0x5565704ffa5b10fdf37d57abfddcf137101d5fb418ded21fa6c5f90262c57dc2'; +const PROXY_ADMIN_BYTECODE_HASH = + '0x7c378e9d49408861ca754fe684b9f7d1ea525bddf095ee0463902df701453ba0'; +const INTERCHAIN_GAS_PAYMASTER_BYTECODE_HASH = + '0xcee48ab556ae2ff12b6458fa92e5e31f4a07f7852a0ed06e43a7f06f3c4c6d76'; +const OVERHEAD_IGP_BYTECODE_HASH = + '0x3cfed1f24f1e9b28a76d5a8c61696a04f7bc474404b823a2fcc210ea52346252'; export class HyperlaneCoreChecker< Chain extends ChainName, > extends HyperlaneAppChecker, CoreConfig> { @@ -31,6 +45,7 @@ export class HyperlaneCoreChecker< await this.checkProxiedContracts(chain); await this.checkMailbox(chain); await this.checkMultisigIsm(chain); + await this.checkBytecodes(chain); await this.checkValidatorAnnounce(chain); } @@ -68,6 +83,78 @@ export class HyperlaneCoreChecker< } } + async checkBytecodes(chain: Chain): Promise { + const contracts = this.app.getContracts(chain); + const mailbox = contracts.mailbox.contract; + const localDomain = await mailbox.localDomain(); + + await this.checkBytecode( + chain, + 'Mailbox implementation', + contracts.mailbox.addresses.implementation, + MAILBOX_WITHOUT_LOCAL_DOMAIN_BYTE_CODE_HASH, + (_) => + // This is obviously super janky but basically we are searching + // for the ocurrences of localDomain in the bytecode and remove + // that to compare, but some coincidental ocurrences of + // localDomain in the bytecode should be not be removed which + // are just done via an offset guard + _.replaceAll( + defaultAbiCoder.encode(['uint32'], [localDomain]).slice(2), + (match, offset) => (offset > 8000 ? match : ''), + ), + ); + + await this.checkBytecode( + chain, + 'Mailbox proxy', + contracts.mailbox.address, + TRANSPARENT_PROXY_BYTECODE_HASH, + ); + await this.checkBytecode( + chain, + 'InterchainGasPaymaster proxy', + contracts.interchainGasPaymaster.address, + TRANSPARENT_PROXY_BYTECODE_HASH, + ); + await this.checkBytecode( + chain, + 'ProxyAdmin', + contracts.proxyAdmin.address, + PROXY_ADMIN_BYTECODE_HASH, + ); + await this.checkBytecode( + chain, + 'MultisigIsm implementation', + contracts.multisigIsm.address, + MULTISIG_ISM_BYTECODE_HASH, + ); + await this.checkBytecode( + chain, + 'InterchainGasPaymaster implementation', + contracts.interchainGasPaymaster.addresses.implementation, + INTERCHAIN_GAS_PAYMASTER_BYTECODE_HASH, + ); + + await this.checkBytecode( + chain, + 'OverheadIGP', + contracts.defaultIsmInterchainGasPaymaster.address, + OVERHEAD_IGP_BYTECODE_HASH, + (_) => + // Remove the address of the wrapped ISM from the bytecode + _.replaceAll( + defaultAbiCoder + .encode( + ['address'], + [contracts.interchainGasPaymaster.addresses.proxy], + ) + .slice(2), + '', + ), + ); + } + async checkProxiedContracts(chain: Chain): Promise { const contracts = this.app.getContracts(chain); await this.checkProxiedContract( diff --git a/typescript/sdk/src/deploy/types.ts b/typescript/sdk/src/deploy/types.ts index 76e8e94f4..76e73c7d8 100644 --- a/typescript/sdk/src/deploy/types.ts +++ b/typescript/sdk/src/deploy/types.ts @@ -20,6 +20,7 @@ export type EnvironmentConfig = ChainMap< export enum ViolationType { Owner = 'Owner', NotDeployed = 'NotDeployed', + BytecodeMismatch = 'BytecodeMismatch', } export interface OwnerViolation extends CheckerViolation { @@ -30,3 +31,8 @@ export interface OwnerViolation extends CheckerViolation { export interface NotDeployedViolation extends CheckerViolation { type: ViolationType.NotDeployed; } + +export interface BytecodeMismatchViolation extends CheckerViolation { + type: ViolationType.BytecodeMismatch; + name: string; +}