feat: ensure consistent decimals in Token checker

trevor/decimal-consistency-checker
Trevor Porter 22 hours ago
parent 58425a2de3
commit 115486e2bd
  1. 23
      typescript/infra/scripts/agent-utils.ts
  2. 3
      typescript/infra/scripts/check/check-utils.ts
  3. 20
      typescript/infra/scripts/check/check-warp-deploy.ts
  4. 22
      typescript/infra/scripts/warp-routes/deploy-warp-monitor.ts
  5. 14
      typescript/sdk/src/deploy/HyperlaneAppChecker.ts
  6. 145
      typescript/sdk/src/token/checker.ts

@ -1,3 +1,4 @@
import { checkbox } from '@inquirer/prompts';
import path, { join } from 'path';
import yargs, { Argv } from 'yargs';
@ -23,6 +24,7 @@ import {
import { Contexts } from '../config/contexts.js';
import { agents } from '../config/environments/agents.js';
import { WarpRouteIds } from '../config/environments/mainnet3/warp/warpIds.js';
import { validatorBaseConfigsFn } from '../config/environments/utils.js';
import {
getChain,
@ -270,6 +272,27 @@ export function withRpcUrls<T>(args: Argv<T>) {
.alias('r', 'rpcUrls');
}
export function withInteractive<T>(args: Argv<T>) {
return args
.describe('interactive', 'If enabled, runs in interactive mode')
.boolean('interactive')
.default('interactive', false);
}
export async function getWarpRouteIdsInteractive() {
const choices = Object.values(WarpRouteIds).map((id) => ({
value: id,
}));
const selection = await checkbox({
message: 'Select Warp Route IDs to deploy',
choices,
pageSize: 30,
});
return selection;
}
// not requiring to build coreConfig to get agentConfig
export async function getAgentConfigsBasedOnArgs(argv?: {
environment: DeployEnvironment;

@ -42,6 +42,7 @@ import {
withContext,
withFork,
withGovern,
withInteractive,
withModule,
withPushMetrics,
withWarpRouteId,
@ -56,7 +57,7 @@ export function getCheckBaseArgs() {
}
export function getCheckWarpDeployArgs() {
return withPushMetrics(getCheckBaseArgs());
return withInteractive(withPushMetrics(getCheckBaseArgs()));
}
export function getCheckDeployArgs() {

@ -3,7 +3,7 @@ import { Gauge, Registry } from 'prom-client';
import { warpConfigGetterMap } from '../../config/warp.js';
import { submitMetrics } from '../../src/utils/metrics.js';
import { Modules } from '../agent-utils.js';
import { Modules, getWarpRouteIdsInteractive } from '../agent-utils.js';
import {
getCheckWarpDeployArgs,
@ -13,8 +13,15 @@ import {
} from './check-utils.js';
async function main() {
const { environment, asDeployer, chains, fork, context, pushMetrics } =
await getCheckWarpDeployArgs().argv;
const {
environment,
asDeployer,
chains,
fork,
context,
pushMetrics,
interactive,
} = await getCheckWarpDeployArgs().argv;
const metricsRegister = new Registry();
const checkerViolationsGauge = new Gauge(
@ -24,8 +31,13 @@ async function main() {
const failedWarpRoutesChecks: string[] = [];
let warpIdsToCheck = Object.keys(warpConfigGetterMap);
if (interactive) {
warpIdsToCheck = await getWarpRouteIdsInteractive();
}
// TODO: consider retrying this if check throws an error
for (const warpRouteId of Object.keys(warpConfigGetterMap)) {
for (const warpRouteId of warpIdsToCheck) {
console.log(`\nChecking warp route ${warpRouteId}...`);
const warpModule = Modules.WARP;

@ -9,6 +9,7 @@ import {
assertCorrectKubeContext,
getAgentConfig,
getArgs,
getWarpRouteIdsInteractive,
withWarpRouteId,
} from '../agent-utils.js';
import { getEnvironmentConfig } from '../core-utils.js';
@ -41,27 +42,6 @@ async function main() {
}
}
async function getWarpRouteIdsInteractive() {
const choices = Object.values(WarpRouteIds).map((id) => ({
value: id,
}));
let selection: WarpRouteIds[] = [];
while (!selection.length) {
selection = await checkbox({
message: 'Select Warp Route IDs to deploy',
choices,
pageSize: 30,
});
if (!selection.length) {
console.log('Please select at least one Warp Route ID');
}
}
return selection;
}
main()
.then(() => console.log('Deploy successful!'))
.catch(console.error);

@ -48,11 +48,7 @@ export abstract class HyperlaneAppChecker<
async check(chainsToCheck?: ChainName[]): Promise<void[]> {
// Get all EVM chains from config
const evmChains = Object.keys(this.configMap).filter(
(chain) =>
this.multiProvider.getChainMetadata(chain).protocol ===
ProtocolType.Ethereum,
);
const evmChains = this.getEvmChains();
// Mark any EVM chains that are not deployed
const appChains = this.app.chains();
@ -82,6 +78,14 @@ export abstract class HyperlaneAppChecker<
);
}
getEvmChains(): ChainName[] {
return Object.keys(this.configMap).filter(
(chain) =>
this.multiProvider.getChainMetadata(chain).protocol ===
ProtocolType.Ethereum,
);
}
addViolation(violation: CheckerViolation): void {
if (violation.type === ViolationType.BytecodeMismatch) {
rootLogger.warn({ violation }, `Found bytecode mismatch. Ignoring...`);

@ -5,8 +5,9 @@ import {
ERC20__factory,
HypERC20Collateral,
IXERC20Lockbox__factory,
TokenRouter,
} from '@hyperlane-xyz/core';
import { eqAddress } from '@hyperlane-xyz/utils';
import { eqAddress, objMap } from '@hyperlane-xyz/utils';
import { TokenMismatchViolation } from '../deploy/types.js';
import { ProxiedRouterChecker } from '../router/ProxiedRouterChecker.js';
@ -66,6 +67,30 @@ export class HypERC20Checker extends ProxiedRouterChecker<
const expectedConfig = this.configMap[chain];
const hypToken = this.app.router(this.app.getContracts(chain));
// Check all actual decimals are consistent
const actualChainDecimals = await this.getAllActualDecimals();
this.checkDecimalConsistency(
chain,
hypToken,
actualChainDecimals,
'actual',
true,
);
// Check all config decimals are consistent as well
const configDecimals = objMap(
this.configMap,
(_chain, config) => config.decimals,
);
this.checkDecimalConsistency(
chain,
hypToken,
configDecimals,
'config',
false,
);
if (isNativeConfig(expectedConfig)) {
try {
await this.multiProvider.estimateGas(chain, {
@ -86,37 +111,123 @@ export class HypERC20Checker extends ProxiedRouterChecker<
} else if (isSyntheticConfig(expectedConfig)) {
await checkERC20(hypToken as unknown as ERC20, expectedConfig);
} else if (isCollateralConfig(expectedConfig)) {
const collateralToken = await this.getCollateralToken(chain);
const actualToken = await (
hypToken as unknown as HypERC20Collateral
).wrappedToken();
if (!eqAddress(collateralToken.address, actualToken)) {
const violation: TokenMismatchViolation = {
type: 'CollateralTokenMismatch',
chain,
expected: collateralToken.address,
actual: actualToken,
tokenAddress: hypToken.address,
};
this.addViolation(violation);
}
}
}
getChainDecimals(): Record<ChainName, number | undefined> {
return objMap(this.configMap, (_chain, config) => config.decimals);
}
private cachedAllActualDecimals: Record<ChainName, number> | undefined =
undefined;
async getAllActualDecimals(): Promise<Record<ChainName, number>> {
if (this.cachedAllActualDecimals) {
return this.cachedAllActualDecimals;
}
const entries = await Promise.all(
this.getEvmChains().map(async (chain) => {
const token = this.app.router(this.app.getContracts(chain));
return [chain, await this.getActualDecimals(chain, token)];
}),
);
this.cachedAllActualDecimals = Object.fromEntries(entries);
return Object.fromEntries(entries);
}
async getActualDecimals(
chain: ChainName,
hypToken: TokenRouter,
): Promise<number> {
const expectedConfig = this.configMap[chain];
let decimals: number | undefined = undefined;
if (isNativeConfig(expectedConfig)) {
decimals =
this.multiProvider.getChainMetadata(chain).nativeToken?.decimals;
} else if (isSyntheticConfig(expectedConfig)) {
decimals = await (hypToken as unknown as ERC20).decimals();
} else if (isCollateralConfig(expectedConfig)) {
const collateralToken = await this.getCollateralToken(chain);
decimals = await collateralToken.decimals();
}
if (!decimals) {
throw new Error('Actual decimals not found');
}
return decimals;
}
async getCollateralToken(chain: ChainName): Promise<ERC20> {
const expectedConfig = this.configMap[chain];
let collateralToken: ERC20 | undefined = undefined;
if (isCollateralConfig(expectedConfig)) {
const provider = this.multiProvider.getProvider(chain);
let collateralToken: ERC20;
if (expectedConfig.type === TokenType.XERC20Lockbox) {
const collateralTokenAddress = await IXERC20Lockbox__factory.connect(
expectedConfig.token,
provider,
).callStatic.ERC20();
collateralToken = await ERC20__factory.connect(
collateralToken = ERC20__factory.connect(
collateralTokenAddress,
provider,
);
} else {
collateralToken = await ERC20__factory.connect(
collateralToken = ERC20__factory.connect(
expectedConfig.token,
provider,
);
}
const actualToken = await (
hypToken as unknown as HypERC20Collateral
).wrappedToken();
if (!eqAddress(collateralToken.address, actualToken)) {
const violation: TokenMismatchViolation = {
type: 'CollateralTokenMismatch',
chain,
expected: collateralToken.address,
actual: actualToken,
tokenAddress: hypToken.address,
};
this.addViolation(violation);
}
}
if (!collateralToken) {
throw new Error('Collateral token not found');
}
return collateralToken;
}
async checkDecimalConsistency(
chain: ChainName,
hypToken: TokenRouter,
chainDecimals: Record<ChainName, number | undefined>,
decimalType: string,
nonEmpty: boolean,
): Promise<void> {
const uniqueChainDecimals = new Set(
Object.values(chainDecimals).filter((decimals) => !!decimals),
);
if (
uniqueChainDecimals.size > 1 ||
(nonEmpty && uniqueChainDecimals.size === 0)
) {
const violation: TokenMismatchViolation = {
type: 'TokenDecimalsMismatch',
chain,
expected: `${
nonEmpty ? 'non-empty and ' : ''
}consistent ${decimalType} decimals`,
actual: JSON.stringify(chainDecimals),
tokenAddress: hypToken.address,
};
this.addViolation(violation);
}
}
}

Loading…
Cancel
Save