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
parent
b110a73f80
commit
37d49ec581
@ -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" |
||||
} |
||||
} |
||||
|
@ -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); |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue