feat: add HypERC20 checker (#3517)

### Description

- Added HypERC20App (extends GasRouterApp) and HypERC20Checker (extends
RouterChecker bc there's no GasRouterChecker - link to issue)
- remoteChains fetches the remote chains from the local router address
is it given. This means, the abstract routerAddress in multiGeneric had
to be made and this call had to awaited in a few places where it's used.
**Why?** previously we assumed that the routers will be fully-connected
and will match 1:1 with the config. This is too strong of an assumption
for ICAs given we inherit the config through `getRouterConfig` and the
ICA accounts will only need to exist on chains which don't have safes.
Plus, in some cases, the chains implied by the app factories vs checker
config don't match and this leads to further issues down the line while
checking for mailbox client properties or in the govern. Checking the
onchain enrollments is the most safe and resilient approach we can take
especially given that having a checker for every RouterApp isn't
feasible.

### Drive-by changes

- adding native keys to warp artifacts for the appHelper to work in
App.fromAddressMap. Note: if there's something using the router key,
this isn't breaking unless you expect only one key.

### Related issues

- fixes https://github.com/hyperlane-xyz/issues/issues/1191

### Backward compatibility

Yes

### Testing

Manual

---------

Signed-off-by: Paul Balaji <paul@hyperlane.xyz>
Co-authored-by: Paul Balaji <paul@hyperlane.xyz>
Co-authored-by: J M Rossy <jm.rossy@gmail.com>
Co-authored-by: Trevor Porter <trkporter@ucdavis.edu>
Co-authored-by: Daniel Savu <23065004+daniel-savu@users.noreply.github.com>
kunal/verify-ica
Kunal Arora 8 months ago committed by GitHub
parent b110a73f80
commit 37d49ec581
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      typescript/helloworld/src/app/app.ts
  2. 3
      typescript/helloworld/src/multiProtocolApp/multiProtocolApp.ts
  3. 3
      typescript/infra/config/environments/mainnet3/warp/addresses.json
  4. 4
      typescript/infra/config/environments/testnet4/warp/addresses.json
  5. 2
      typescript/infra/hardhat.config.ts
  6. 40
      typescript/infra/scripts/check-deploy.ts
  7. 6
      typescript/infra/src/govern/HyperlaneAppGovernor.ts
  8. 3
      typescript/sdk/src/core/TestCoreApp.ts
  9. 5
      typescript/sdk/src/deploy/types.ts
  10. 2
      typescript/sdk/src/gas/HyperlaneIgpChecker.ts
  11. 2
      typescript/sdk/src/index.ts
  12. 4
      typescript/sdk/src/router/HyperlaneRouterChecker.ts
  13. 16
      typescript/sdk/src/router/RouterApps.ts
  14. 47
      typescript/sdk/src/token/app.ts
  15. 102
      typescript/sdk/src/token/checker.ts
  16. 6
      typescript/sdk/src/token/contracts.ts
  17. 2
      typescript/sdk/src/utils/MultiGeneric.ts

@ -95,8 +95,9 @@ export class HelloWorldApp extends RouterApp<HelloWorldFactories> {
async stats(): Promise<ChainMap<ChainMap<StatCounts>>> {
const entries: Array<[ChainName, ChainMap<StatCounts>]> = await Promise.all(
this.chains().map(async (source) => {
const remoteChains = await this.remoteChains(source);
const destinationEntries = await Promise.all(
this.remoteChains(source).map(async (destination) => [
remoteChains.map(async (destination) => [
destination,
await this.channelStats(source, destination),
]),

@ -52,8 +52,9 @@ export class HelloMultiProtocolApp extends MultiProtocolRouterApp<
async stats(): Promise<ChainMap<ChainMap<StatCounts>>> {
const entries: Array<[ChainName, ChainMap<StatCounts>]> = await Promise.all(
this.chains().map(async (source) => {
const remoteChains = await this.remoteChains(source);
const destinationEntries = await Promise.all(
this.remoteChains(source).map(async (destination) => [
remoteChains.map(async (destination) => [
destination,
await this.channelStats(source, destination),
]),

@ -1,9 +1,10 @@
{
"injective": {
"native": "inj1mv9tjvkaw7x8w8y9vds8pkfq46g2vcfkjehc6k",
"router": "inj1mv9tjvkaw7x8w8y9vds8pkfq46g2vcfkjehc6k"
},
"inevm": {
"HypNative": "0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4",
"native": "0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4",
"router": "0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4"
}
}

@ -1,8 +1,8 @@
{
"plumetestnet": {
"router": "0xD356C996277eFb7f75Ee8bd61b31cC781A12F54f"
"synthetic": "0xD356C996277eFb7f75Ee8bd61b31cC781A12F54f"
},
"sepolia": {
"router": "0xd99eA1D8b9542D35252504DDd59EDe8C43FB15fd"
"native": "0xd99eA1D8b9542D35252504DDd59EDe8C43FB15fd"
}
}

@ -119,7 +119,7 @@ task('kathy', 'Dispatches random hyperlane messages')
// Round robin origin chain
const local = core.chains()[messages % core.chains().length];
// Random remote chain
const remote: ChainName = randomElement(core.remoteChains(local));
const remote: ChainName = randomElement(await core.remoteChains(local));
const remoteId = multiProvider.getDomainId(remote);
const contracts = core.getContracts(local);
const mailbox = contracts.mailbox;

@ -1,5 +1,7 @@
import { HelloWorldChecker } from '@hyperlane-xyz/helloworld';
import {
HypERC20App,
HypERC20Checker,
HyperlaneCore,
HyperlaneCoreChecker,
HyperlaneIgp,
@ -9,6 +11,7 @@ import {
InterchainAccountChecker,
InterchainQuery,
InterchainQueryChecker,
TokenType,
resolveOrDeployAccountOwner,
} from '@hyperlane-xyz/sdk';
@ -23,6 +26,7 @@ import { impersonateAccount, useLocalProvider } from '../src/utils/fork';
import {
Modules,
getAddresses,
getArgs as getRootArgs,
withContext,
withModuleAndFork,
@ -111,6 +115,42 @@ async function check() {
ismFactory,
);
governor = new ProxiedRouterGovernor(checker);
} else if (module === Modules.WARP) {
// test config
const plumetestnet = {
...routerConfig.plumetestnet,
type: TokenType.synthetic,
name: 'Wrapped Ether',
symbol: 'WETH',
decimals: 18,
totalSupply: '0',
gas: 0,
};
const sepolia = {
...routerConfig.sepolia,
type: TokenType.native,
gas: 0,
};
const config = {
plumetestnet,
sepolia,
};
const addresses = getAddresses(environment, Modules.WARP);
const filteredAddresses = Object.keys(addresses) // filter out changes not in config
.filter((key) => key in config)
.reduce((obj, key) => {
obj[key] = addresses[key];
return obj;
}, {} as typeof addresses);
const app = HypERC20App.fromAddressesMap(filteredAddresses, multiProvider);
const checker = new HypERC20Checker(
multiProvider,
app,
config as any,
ismFactory,
);
governor = new ProxiedRouterGovernor(checker, ica);
} else {
console.log(`Skipping ${module}, checker or governor unimplemented`);
return;

@ -30,9 +30,9 @@ import {
} from './multisend';
export enum SubmissionType {
MANUAL = 'MANUAL',
SIGNER = 'SIGNER',
SAFE = 'SAFE',
MANUAL = 0,
SAFE = 1,
SIGNER = 2,
}
export type AnnotatedCallData = CallData & {

@ -28,7 +28,8 @@ export class TestCoreApp extends HyperlaneCore {
for (const origin of this.chains()) {
const outbound = await this.processOutboundMessages(origin);
const originResponses = new Map();
this.remoteChains(origin).forEach((destination) =>
const remoteChains = await this.remoteChains(origin);
remoteChains.forEach((destination) =>
originResponses.set(destination, outbound.get(destination)),
);
responses.set(origin, originResponses);

@ -54,6 +54,7 @@ export enum ViolationType {
ProxyAdmin = 'ProxyAdmin',
TimelockController = 'TimelockController',
AccessControl = 'AccessControl',
TokenMismatch = 'TokenMismatch',
}
export interface OwnerViolation extends CheckerViolation {
@ -93,3 +94,7 @@ export interface BytecodeMismatchViolation extends CheckerViolation {
type: ViolationType.BytecodeMismatch;
name: string;
}
export interface TokenMismatchViolation extends CheckerViolation {
tokenAddress: Address;
}

@ -80,7 +80,7 @@ export class HyperlaneIgpChecker extends HyperlaneAppChecker<
expected: {},
};
const remotes = this.app.remoteChains(local);
const remotes = await this.app.remoteChains(local);
for (const remote of remotes) {
let expectedOverhead = this.configMap[local].overhead[remote];
if (!expectedOverhead) {

@ -368,6 +368,8 @@ export {
SealevelTransferRemoteInstruction,
SealevelTransferRemoteSchema,
} from './token/adapters/serialization';
export { HypERC20App } from './token/app';
export { HypERC20Checker } from './token/checker';
export {
CollateralConfig,
ERC20Metadata,

@ -141,9 +141,9 @@ export class HyperlaneRouterChecker<
async checkEnrolledRouters(chain: ChainName): Promise<void> {
const router = this.app.router(this.app.getContracts(chain));
const remoteChains = await this.app.remoteChains(chain);
await Promise.all(
this.app.remoteChains(chain).map(async (remoteChain) => {
remoteChains.map(async (remoteChain) => {
const remoteRouterAddress = this.app.routerAddress(remoteChain);
const remoteDomainId = this.multiProvider.getDomainId(remoteChain);
const actualRouter = await router.routers(remoteDomainId);

@ -44,11 +44,17 @@ export abstract class RouterApp<
return this.foreignDeployments[chainName];
}
override remoteChains(chainName: string): string[] {
return [
...super.remoteChains(chainName),
...Object.keys(this.foreignDeployments),
].filter((chain) => chain !== chainName);
// check onchain for remote enrollments
override async remoteChains(chainName: string): Promise<ChainName[]> {
const router = this.router(this.contractsMap[chainName]);
const domainIds = (await router.domains()).map((domain) => {
const chainName = this.multiProvider.tryGetChainName(domain);
if (chainName === null) {
throw new Error(`Chain name not found for domain: ${domain}`);
}
return chainName;
});
return domainIds;
}
getSecurityModules(): Promise<ChainMap<Address>> {

@ -0,0 +1,47 @@
import { TokenRouter } from '@hyperlane-xyz/core';
import { objKeys } from '@hyperlane-xyz/utils';
import { appFromAddressesMapHelper } from '../contracts/contracts';
import {
HyperlaneAddressesMap,
HyperlaneContracts,
HyperlaneContractsMap,
} from '../contracts/types';
import { MultiProvider } from '../providers/MultiProvider';
import { GasRouterApp } from '../router/RouterApps';
import {
HypERC20Factories,
hypERC20Tokenfactories,
hypERC20factories,
} from './contracts';
export class HypERC20App extends GasRouterApp<HypERC20Factories, TokenRouter> {
constructor(
contractsMap: HyperlaneContractsMap<HypERC20Factories>,
multiProvider: MultiProvider,
) {
super(contractsMap, multiProvider);
}
router(contracts: HyperlaneContracts<HypERC20Factories>): TokenRouter {
for (const key of objKeys(hypERC20Tokenfactories)) {
if (contracts[key]) {
return contracts[key] as unknown as TokenRouter;
}
}
throw new Error('No router found in contracts');
}
static fromAddressesMap(
addressesMap: HyperlaneAddressesMap<HypERC20Factories>,
multiProvider: MultiProvider,
): HypERC20App {
const helper = appFromAddressesMapHelper(
addressesMap,
hypERC20factories,
multiProvider,
);
return new HypERC20App(helper.contractsMap, helper.multiProvider);
}
}

@ -0,0 +1,102 @@
import { BigNumber } from 'ethers';
import { ERC20, ERC20__factory, HypERC20Collateral } from '@hyperlane-xyz/core';
import { eqAddress } from '@hyperlane-xyz/utils';
import { TokenMismatchViolation } from '../deploy/types';
import { HyperlaneRouterChecker } from '../router/HyperlaneRouterChecker';
import { ChainName } from '../types';
import { HypERC20App } from './app';
import {
ERC20RouterConfig,
HypERC20Config,
TokenMetadata,
isCollateralConfig,
isNativeConfig,
isSyntheticConfig,
} from './config';
import { HypERC20Factories } from './contracts';
export class HypERC20Checker extends HyperlaneRouterChecker<
HypERC20Factories,
HypERC20App,
ERC20RouterConfig
> {
async checkChain(chain: ChainName): Promise<void> {
await super.checkChain(chain);
await this.checkToken(chain);
}
async checkToken(chain: ChainName): Promise<void> {
const checkERC20 = async (
token: ERC20,
config: HypERC20Config,
): Promise<void> => {
const checks: {
method: keyof TokenMetadata | 'decimals';
violationType: string;
}[] = [
{ method: 'symbol', violationType: 'TokenSymbolMismatch' },
{ method: 'name', violationType: 'TokenNameMismatch' },
{ method: 'decimals', violationType: 'TokenDecimalsMismatch' },
];
for (const check of checks) {
const actual = await token[check.method]();
const expected = config[check.method];
if (actual !== expected) {
const violation: TokenMismatchViolation = {
type: check.violationType,
chain,
expected,
actual,
tokenAddress: token.address,
};
this.addViolation(violation);
}
}
};
const expectedConfig = this.configMap[chain];
const hypToken = this.app.router(this.app.getContracts(chain));
if (isNativeConfig(expectedConfig)) {
try {
await this.multiProvider.estimateGas(chain, {
to: hypToken.address,
from: await this.multiProvider.getSignerAddress(chain),
value: BigNumber.from(1),
});
} catch (e) {
const violation: TokenMismatchViolation = {
type: 'deployed token not payable',
chain,
expected: 'true',
actual: 'false',
tokenAddress: hypToken.address,
};
this.addViolation(violation);
}
} else if (isSyntheticConfig(expectedConfig)) {
await checkERC20(hypToken as unknown as ERC20, expectedConfig);
} else if (isCollateralConfig(expectedConfig)) {
const collateralToken = await ERC20__factory.connect(
expectedConfig.token,
this.multiProvider.getProvider(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);
}
}
}
}

@ -27,7 +27,7 @@ export const hypERC20contracts = {
};
export type HypERC20contracts = typeof hypERC20contracts;
export const hypERC20factories = {
export const hypERC20Tokenfactories = {
[TokenType.fastCollateral]: new FastHypERC20Collateral__factory(),
[TokenType.fastSynthetic]: new FastHypERC20__factory(),
[TokenType.synthetic]: new HypERC20__factory(),
@ -35,6 +35,10 @@ export const hypERC20factories = {
[TokenType.collateralVault]: new HypERC20CollateralVaultDeposit__factory(),
[TokenType.native]: new HypNative__factory(),
[TokenType.nativeScaled]: new HypNativeScaled__factory(),
};
export const hypERC20factories = {
...hypERC20Tokenfactories,
...proxiedFactories,
};
export type HypERC20Factories = typeof hypERC20factories;

@ -54,7 +54,7 @@ export class MultiGeneric<Value> {
return Object.fromEntries(entries);
}
remoteChains(name: ChainName): ChainName[] {
async remoteChains(name: ChainName): Promise<ChainName[]> {
return this.chains().filter((key) => key !== name);
}

Loading…
Cancel
Save