From 6cf3982589e58fc7d92277a28236f19f998f37cc Mon Sep 17 00:00:00 2001 From: Mohammed Hussan Date: Tue, 9 Jul 2024 15:43:09 +0100 Subject: [PATCH] feat(infra): Add xERC20 limits monitoring capabilities (#4071) ### Description - Add support for pushing limits metrics for xERC20 wrap routes - For now only push limits metrics for xERC20 wrap routes, not yet supporting warp route balances ### Drive-by changes ### Related issues - Fixes #4047 ### Backward compatibility ### Testing Manual --- solidity/contracts/test/ERC20Test.sol | 12 ++ .../contracts/token/interfaces/IXERC20.sol | 21 ++ .../mainnet3/warp/EZETH-deployments.yaml | 69 +++++++ .../monitor-warp-routes-balances.ts | 185 ++++++++++++++++-- 4 files changed, 273 insertions(+), 14 deletions(-) create mode 100644 typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml diff --git a/solidity/contracts/test/ERC20Test.sol b/solidity/contracts/test/ERC20Test.sol index b9e43a7ac..a825c8d13 100644 --- a/solidity/contracts/test/ERC20Test.sol +++ b/solidity/contracts/test/ERC20Test.sol @@ -86,6 +86,18 @@ contract XERC20Test is ERC20Test, IXERC20 { ) external view returns (uint256) { return type(uint256).max; } + + function mintingMaxLimitOf( + address _bridge + ) external view returns (uint256) { + return type(uint256).max; + } + + function burningMaxLimitOf( + address _bridge + ) external view returns (uint256) { + return type(uint256).max; + } } contract XERC20LockboxTest is IXERC20Lockbox { diff --git a/solidity/contracts/token/interfaces/IXERC20.sol b/solidity/contracts/token/interfaces/IXERC20.sol index 61b03e46b..b933af182 100644 --- a/solidity/contracts/token/interfaces/IXERC20.sol +++ b/solidity/contracts/token/interfaces/IXERC20.sol @@ -54,4 +54,25 @@ interface IXERC20 is IERC20 { function mintingCurrentLimitOf( address _bridge ) external view returns (uint256 _limit); + + /** + * @notice Returns the max limit of a minter + * + * @param _minter The minter we are viewing the limits of + * @return _limit The limit the minter has + */ + function mintingMaxLimitOf( + address _minter + ) external view returns (uint256 _limit); + + /** + * @notice Returns the max limit of a bridge + * + * @param _bridge the bridge we are viewing the limits of + * @return _limit The limit the bridge has + */ + + function burningMaxLimitOf( + address _bridge + ) external view returns (uint256 _limit); } diff --git a/typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml new file mode 100644 index 000000000..4f0e00246 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml @@ -0,0 +1,69 @@ +description: Hyperlane Warp Route artifacts +timestamp: '2024-06-04T16:00:00.000Z' +deployer: Abacus Works (Hyperlane) +data: + config: + ethereum: + protocolType: ethereum + type: xERC20Lockbox + name: Renzo Restaked ETH + symbol: ezETH + hypAddress: '0xdFf621F952c23972dFD3A9E5d7B9f6339e9c078B' + tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5' + decimals: 18 + bsc: + protocolType: ethereum + type: xERC20 + name: Renzo Restaked ETH + symbol: ezETH + hypAddress: '0x6266e803057fa68C35018C3FB0B59db7129C23BB' + tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5' + decimals: 18 + arbitrum: + protocolType: ethereum + type: xERC20 + name: Renzo Restaked ETH + symbol: ezETH + hypAddress: '0xC8F280d3eC30746f77c28695827d309d16939BF1' + tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5' + decimals: 18 + optimism: + protocolType: ethereum + type: xERC20 + name: Renzo Restaked ETH + symbol: ezETH + hypAddress: '0x1d1a210E71398c17FD7987eDF1dc347539bB541F' + tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5' + decimals: 18 + base: + protocolType: ethereum + type: xERC20 + name: Renzo Restaked ETH + symbol: ezETH + hypAddress: '0x584BA77ec804f8B6A559D196661C0242C6844F49' + tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5' + decimals: 18 + blast: + protocolType: ethereum + type: xERC20 + name: Renzo Restaked ETH + symbol: ezETH + hypAddress: '0x8C603c6BDf8a9d548fC5D2995750Cc25eF59183b' + tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5' + decimals: 18 + mode: + protocolType: ethereum + type: xERC20 + name: Renzo Restaked ETH + symbol: ezETH + hypAddress: '0xcd95B8dF351400BF4cbAb340b6EfF2454aDB299E' + tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5' + decimals: 18 + linea: + protocolType: ethereum + type: xERC20 + name: Renzo Restaked ETH + symbol: ezETH + hypAddress: '0xcd95B8dF351400BF4cbAb340b6EfF2454aDB299E' + tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5' + decimals: 18 diff --git a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts index e7ebfbc28..478dfc232 100644 --- a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts +++ b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts @@ -3,6 +3,12 @@ import { ethers } from 'ethers'; import { Gauge, Registry } from 'prom-client'; import yargs from 'yargs'; +import { + HypXERC20Lockbox__factory, + HypXERC20__factory, + IXERC20, + IXERC20__factory, +} from '@hyperlane-xyz/core'; import { ERC20__factory } from '@hyperlane-xyz/core'; import { ChainMap, @@ -42,6 +48,20 @@ const warpRouteTokenBalance = new Gauge({ ], }); +const xERC20LimitsGauge = new Gauge({ + name: 'hyperlane_xerc20_limits', + help: 'Current minting and burning limits of xERC20 tokens', + registers: [metricsRegister], + labelNames: ['chain_name', 'limit_type'], +}); + +interface xERC20Limit { + mint: number; + burn: number; + mintMax: number; + burnMax: number; +} + export function readWarpRouteConfig(filePath: string) { const config = readYaml(filePath); if (!config) throw new Error(`No warp config found at ${filePath}`); @@ -70,23 +90,24 @@ async function main(): Promise { .string('filePath') .parse(); + startMetricsServer(metricsRegister); + const tokenConfig: WarpRouteConfig = readWarpRouteConfig(filePath).data.config; - startMetricsServer(metricsRegister); - - logger.info('Starting Warp Route balance monitor'); - const multiProtocolProvider = new MultiProtocolProvider(getChainMetadata()); + // TODO: eventually support token balance checks for xERC20 token type also + if ( + Object.values(tokenConfig).some( + (token) => + token.type === TokenType.XERC20 || + token.type === TokenType.XERC20Lockbox, + ) + ) { + await checkXERC20Limits(checkFrequency, tokenConfig); + } else { + await checkTokenBalances(checkFrequency, tokenConfig); + } - setInterval(async () => { - try { - logger.debug('Checking balances'); - const balances = await checkBalance(tokenConfig, multiProtocolProvider); - updateTokenBalanceMetrics(tokenConfig, balances); - } catch (e) { - logger.error('Error checking balances', e); - } - }, checkFrequency); return true; } @@ -215,7 +236,7 @@ async function checkBalance( }, ); - return await promiseObjAll(output); + return promiseObjAll(output); } export function updateTokenBalanceMetrics( @@ -240,4 +261,140 @@ export function updateTokenBalanceMetrics( }); } +export function updateXERC20LimitsMetrics(xERC20Limits: ChainMap) { + objMap(xERC20Limits, (chain: ChainName, limit: xERC20Limit) => { + xERC20LimitsGauge + .labels({ + chain_name: chain, + limit_type: 'mint', + }) + .set(limit.mint); + xERC20LimitsGauge + .labels({ + chain_name: chain, + limit_type: 'burn', + }) + .set(limit.burn); + xERC20LimitsGauge + .labels({ + chain_name: chain, + limit_type: 'mintMax', + }) + .set(limit.mintMax); + xERC20LimitsGauge + .labels({ + chain_name: chain, + limit_type: 'burnMax', + }) + .set(limit.burnMax); + logger.info('xERC20 limits updated for chain', { + chain, + mint: limit.mint, + burn: limit.burn, + mintMax: limit.mintMax, + burnMax: limit.burnMax, + }); + }); +} + +async function getXERC20Limits( + tokenConfig: WarpRouteConfig, +): Promise> { + const multiProtocolProvider = new MultiProtocolProvider(getChainMetadata()); + + const output = objMap( + tokenConfig, + async (chain: ChainName, token: WarpRouteConfig[ChainName]) => { + switch (token.protocolType) { + case ProtocolType.Ethereum: { + switch (token.type) { + case TokenType.XERC20Lockbox: { + const provider = multiProtocolProvider.getEthersV5Provider(chain); + const routerAddress = token.hypAddress; + const lockbox = HypXERC20Lockbox__factory.connect( + token.hypAddress, + provider, + ); + const xerc20Address = await lockbox.xERC20(); + const xerc20 = IXERC20__factory.connect(xerc20Address, provider); + return getXERC20Limit(routerAddress, xerc20, token.decimals); + } + case TokenType.XERC20: { + const provider = multiProtocolProvider.getEthersV5Provider(chain); + const routerAddress = token.hypAddress; + const hypXERC20 = HypXERC20__factory.connect( + routerAddress, + provider, + ); + const xerc20Address = await hypXERC20.wrappedToken(); + const xerc20 = IXERC20__factory.connect(xerc20Address, provider); + return getXERC20Limit(routerAddress, xerc20, token.decimals); + } + } + break; + } + } + return { + chain: chain, + mint: 0, + mintMax: 0, + burn: 0, + burnMax: 0, + }; + }, + ); + + return promiseObjAll(output); +} + +const getXERC20Limit = async ( + routerAddress: string, + xerc20: IXERC20, + decimals: number, +): Promise => { + const mintCurrent = await xerc20.mintingCurrentLimitOf(routerAddress); + const mintMax = await xerc20.mintingMaxLimitOf(routerAddress); + const burnCurrent = await xerc20.burningCurrentLimitOf(routerAddress); + const burnMax = await xerc20.burningMaxLimitOf(routerAddress); + return { + mint: parseFloat(ethers.utils.formatUnits(mintCurrent, decimals)), + mintMax: parseFloat(ethers.utils.formatUnits(mintMax, decimals)), + burn: parseFloat(ethers.utils.formatUnits(burnCurrent, decimals)), + burnMax: parseFloat(ethers.utils.formatUnits(burnMax, decimals)), + }; +}; + +async function checkXERC20Limits( + checkFrequency: number, + tokenConfig: WarpRouteConfig, +) { + setInterval(async () => { + try { + const xERC20Limits = await getXERC20Limits(tokenConfig); + logger.info('xERC20 Limits:', xERC20Limits); + updateXERC20LimitsMetrics(xERC20Limits); + } catch (e) { + logger.error('Error checking balances', e); + } + }, checkFrequency); +} + +async function checkTokenBalances( + checkFrequency: number, + tokenConfig: WarpRouteConfig, +) { + logger.info('Starting Warp Route balance monitor'); + const multiProtocolProvider = new MultiProtocolProvider(getChainMetadata()); + + setInterval(async () => { + try { + logger.debug('Checking balances'); + const balances = await checkBalance(tokenConfig, multiProtocolProvider); + updateTokenBalanceMetrics(tokenConfig, balances); + } catch (e) { + logger.error('Error checking balances', e); + } + }, checkFrequency); +} + main().then(logger.info).catch(logger.error);