@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
295 lines • 13.2 kB
JavaScript
import { BigNumber, Contract, constants } from 'ethers';
import { HypERC20Collateral__factory, HypERC20__factory, HypERC4626Collateral__factory, HypERC4626OwnerCollateral__factory, HypERC4626__factory, HypXERC20Lockbox__factory, IXERC20__factory, ProxyAdmin__factory, TokenRouter__factory, } from '@hyperlane-xyz/core';
import { bytes32ToAddress, eqAddress, getLogLevel, rootLogger, } from '@hyperlane-xyz/utils';
import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js';
import { EvmHookReader } from '../hook/EvmHookReader.js';
import { EvmIsmReader } from '../ism/EvmIsmReader.js';
import { RemoteRoutersSchema, } from '../router/types.js';
import { HyperlaneReader } from '../utils/HyperlaneReader.js';
import { proxyAdmin } from './../deploy/proxy.js';
import { NON_ZERO_SENDER_ADDRESS, TokenType } from './config.js';
import { getExtraLockBoxConfigs } from './xerc20.js';
export class EvmERC20WarpRouteReader extends HyperlaneReader {
multiProvider;
chain;
concurrency;
logger = rootLogger.child({
module: 'EvmERC20WarpRouteReader',
});
evmHookReader;
evmIsmReader;
constructor(multiProvider, chain, concurrency = DEFAULT_CONTRACT_READ_CONCURRENCY) {
super(multiProvider, chain);
this.multiProvider = multiProvider;
this.chain = chain;
this.concurrency = concurrency;
this.evmHookReader = new EvmHookReader(multiProvider, chain, concurrency);
this.evmIsmReader = new EvmIsmReader(multiProvider, chain, concurrency);
}
/**
* Derives the configuration for a Hyperlane ERC20 router contract at the given address.
*
* @param warpRouteAddress - The address of the Hyperlane ERC20 router contract.
* @returns The configuration for the Hyperlane ERC20 router.
*
*/
async deriveWarpRouteConfig(warpRouteAddress) {
// Derive the config type
const type = await this.deriveTokenType(warpRouteAddress);
const mailboxClientConfig = await this.fetchMailboxClientConfig(warpRouteAddress);
const tokenConfig = await this.fetchTokenConfig(type, warpRouteAddress);
const remoteRouters = await this.fetchRemoteRouters(warpRouteAddress);
const proxyAdmin = await this.fetchProxyAdminConfig(warpRouteAddress);
const destinationGas = await this.fetchDestinationGas(warpRouteAddress);
return {
...mailboxClientConfig,
...tokenConfig,
remoteRouters,
proxyAdmin,
destinationGas,
};
}
/**
* Derives the token type for a given Warp Route address using specific methods
*
* @param warpRouteAddress - The Warp Route address to derive the token type for.
* @returns The derived token type, which can be one of: collateralVault, collateral, native, or synthetic.
*/
async deriveTokenType(warpRouteAddress) {
const contractTypes = {
[TokenType.collateralVaultRebase]: {
factory: HypERC4626Collateral__factory,
method: 'NULL_RECIPIENT',
},
[TokenType.collateralVault]: {
factory: HypERC4626OwnerCollateral__factory,
method: 'vault',
},
[TokenType.XERC20Lockbox]: {
factory: HypXERC20Lockbox__factory,
method: 'lockbox',
},
[TokenType.collateral]: {
factory: HypERC20Collateral__factory,
method: 'wrappedToken',
},
[TokenType.syntheticRebase]: {
factory: HypERC4626__factory,
method: 'collateralDomain',
},
[TokenType.synthetic]: {
factory: HypERC20__factory,
method: 'decimals',
},
};
// Temporarily turn off SmartProvider logging
// Provider errors are expected because deriving will call methods that may not exist in the Bytecode
this.setSmartProviderLogLevel('silent');
// First, try checking token specific methods
for (const [tokenType, { factory, method }] of Object.entries(contractTypes)) {
try {
const warpRoute = factory.connect(warpRouteAddress, this.provider);
await warpRoute[method]();
if (tokenType === TokenType.collateral) {
const wrappedToken = await warpRoute.wrappedToken();
const xerc20 = IXERC20__factory.connect(wrappedToken, this.provider);
try {
await xerc20['mintingCurrentLimitOf(address)'](warpRouteAddress);
return TokenType.XERC20;
// eslint-disable-next-line no-empty
}
catch { }
}
return tokenType;
}
catch {
continue;
}
finally {
this.setSmartProviderLogLevel(getLogLevel()); // returns to original level defined by rootLogger
}
}
// Finally check native
// Using estimateGas to send 0 wei. Success implies that the Warp Route has a receive() function
try {
await this.multiProvider.estimateGas(this.chain, {
to: warpRouteAddress,
value: BigNumber.from(0),
}, NON_ZERO_SENDER_ADDRESS);
return TokenType.native;
}
catch (e) {
throw Error(`Error accessing token specific method ${e}`);
}
finally {
this.setSmartProviderLogLevel(getLogLevel()); // returns to original level defined by rootLogger
}
}
/**
* Fetches the base metadata for a Warp Route contract.
*
* @param routerAddress - The address of the Warp Route contract.
* @returns The base metadata for the Warp Route contract, including the mailbox, owner, hook, and ism.
*/
async fetchMailboxClientConfig(routerAddress) {
const warpRoute = HypERC20Collateral__factory.connect(routerAddress, this.provider);
const [mailbox, owner, hook, ism] = await Promise.all([
warpRoute.mailbox(),
warpRoute.owner(),
warpRoute.hook(),
warpRoute.interchainSecurityModule(),
]);
const derivedIsm = eqAddress(ism, constants.AddressZero)
? constants.AddressZero
: await this.evmIsmReader.deriveIsmConfig(ism);
const derivedHook = eqAddress(hook, constants.AddressZero)
? constants.AddressZero
: await this.evmHookReader.deriveHookConfig(hook);
return {
mailbox,
owner,
hook: derivedHook,
interchainSecurityModule: derivedIsm,
};
}
async fetchXERC20Config(xERC20Address, warpRouteAddress) {
// fetch the limits if possible
const rateLimitsABI = [
'function rateLimitPerSecond(address) external view returns (uint128)',
'function bufferCap(address) external view returns (uint112)',
];
const xERC20 = new Contract(xERC20Address, rateLimitsABI, this.provider);
try {
const extraBridgesLimits = await getExtraLockBoxConfigs({
chain: this.chain,
multiProvider: this.multiProvider,
xERC20Address,
logger: this.logger,
});
return {
xERC20: {
warpRouteLimits: {
rateLimitPerSecond: (await xERC20.rateLimitPerSecond(warpRouteAddress)).toString(),
bufferCap: (await xERC20.bufferCap(warpRouteAddress)).toString(),
},
extraBridges: extraBridgesLimits.length > 0 ? extraBridgesLimits : undefined,
},
};
}
catch (error) {
this.logger.error(`Error fetching xERC20 limits for token at ${xERC20Address} on chain ${this.chain}`, error);
return {};
}
}
/**
* Fetches the metadata for a token address.
*
* @param warpRouteAddress - The address of the token.
* @returns A partial ERC20 metadata object containing the token name, symbol, total supply, and decimals.
* Throws if unsupported token type
*/
async fetchTokenConfig(type, warpRouteAddress) {
if (type === TokenType.collateral ||
type === TokenType.collateralVault ||
type === TokenType.collateralVaultRebase ||
type === TokenType.XERC20 ||
type === TokenType.XERC20Lockbox) {
let xerc20Token;
let lockbox;
let token;
let xERC20Metadata = {};
if (type === TokenType.XERC20Lockbox) {
// XERC20Lockbox is a special case of collateral, we will fetch it from the xerc20 contract
const hypXERC20Lockbox = HypXERC20Lockbox__factory.connect(warpRouteAddress, this.provider);
xerc20Token = await hypXERC20Lockbox.xERC20();
token = xerc20Token;
lockbox = await hypXERC20Lockbox.lockbox();
}
else {
const erc20 = HypERC20Collateral__factory.connect(warpRouteAddress, this.provider);
token = await erc20.wrappedToken();
}
const { name, symbol, decimals } = await this.fetchERC20Metadata(token);
if (type === TokenType.XERC20 || type === TokenType.XERC20Lockbox) {
xERC20Metadata = await this.fetchXERC20Config(token, warpRouteAddress);
}
return {
...xERC20Metadata,
type,
name,
symbol,
decimals,
token: lockbox || token,
};
}
else if (type === TokenType.synthetic ||
type === TokenType.syntheticRebase) {
const baseMetadata = await this.fetchERC20Metadata(warpRouteAddress);
if (type === TokenType.syntheticRebase) {
const hypERC4626 = HypERC4626__factory.connect(warpRouteAddress, this.provider);
const collateralChainName = this.multiProvider.getChainName(await hypERC4626.collateralDomain());
return { type, ...baseMetadata, collateralChainName };
}
return { type, ...baseMetadata };
}
else if (type === TokenType.native) {
const chainMetadata = this.multiProvider.getChainMetadata(this.chain);
if (chainMetadata.nativeToken) {
const { name, symbol, decimals } = chainMetadata.nativeToken;
return {
type,
name,
symbol,
decimals,
};
}
else {
throw new Error(`Warp route config specifies native token but chain metadata for ${this.chain} does not provide native token details`);
}
}
else {
throw new Error(`Unsupported token type ${type} when fetching token metadata`);
}
}
async fetchERC20Metadata(tokenAddress) {
const erc20 = HypERC20__factory.connect(tokenAddress, this.provider);
const [name, symbol, decimals] = await Promise.all([
erc20.name(),
erc20.symbol(),
erc20.decimals(),
]);
return { name, symbol, decimals };
}
async fetchRemoteRouters(warpRouteAddress) {
const warpRoute = TokenRouter__factory.connect(warpRouteAddress, this.provider);
const domains = await warpRoute.domains();
const routers = Object.fromEntries(await Promise.all(domains.map(async (domain) => {
return [
domain,
{ address: bytes32ToAddress(await warpRoute.routers(domain)) },
];
})));
return RemoteRoutersSchema.parse(routers);
}
async fetchProxyAdminConfig(tokenAddress) {
const proxyAdminAddress = await proxyAdmin(this.provider, tokenAddress);
const proxyAdminInstance = ProxyAdmin__factory.connect(proxyAdminAddress, this.provider);
return {
address: proxyAdminAddress,
owner: await proxyAdminInstance.owner(),
};
}
async fetchDestinationGas(warpRouteAddress) {
const warpRoute = TokenRouter__factory.connect(warpRouteAddress, this.provider);
/**
* @remark
* Router.domains() is used to enumerate the destination gas because GasRouter.destinationGas is not EnumerableMapExtended type
* This means that if a domain is removed, then we cannot read the destinationGas for it. This may impact updates.
*/
const domains = await warpRoute.domains();
return Object.fromEntries(await Promise.all(domains.map(async (domain) => {
return [domain, (await warpRoute.destinationGas(domain)).toString()];
})));
}
}
//# sourceMappingURL=EvmERC20WarpRouteReader.js.map