feat(infra): Add xERC20 limits monitoring capabilities (#4071)

### Description

<!--
What's included in this PR?
-->

- 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

<!--
Are there any minor or drive-by changes also included?
-->

### Related issues

<!--
- Fixes #[issue number here]
-->
 - Fixes #4047 
### Backward compatibility

<!--
Are these changes backward compatible? Are there any infrastructure
implications, e.g. changes that would prohibit deploying older commits
using this infra tooling?

Yes/No
-->

### Testing

<!--
What kind of testing have these changes undergone?

None/Manual/Unit Tests
-->
Manual
pull/3818/head
Mohammed Hussan 5 months ago committed by GitHub
parent be4617b18b
commit 6cf3982589
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 12
      solidity/contracts/test/ERC20Test.sol
  2. 21
      solidity/contracts/token/interfaces/IXERC20.sol
  3. 69
      typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml
  4. 185
      typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts

@ -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 {

@ -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);
}

@ -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

@ -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<boolean> {
.string('filePath')
.parse();
const tokenConfig: WarpRouteConfig =
readWarpRouteConfig(filePath).data.config;
startMetricsServer(metricsRegister);
logger.info('Starting Warp Route balance monitor');
const multiProtocolProvider = new MultiProtocolProvider(getChainMetadata());
const tokenConfig: WarpRouteConfig =
readWarpRouteConfig(filePath).data.config;
setInterval(async () => {
try {
logger.debug('Checking balances');
const balances = await checkBalance(tokenConfig, multiProtocolProvider);
updateTokenBalanceMetrics(tokenConfig, balances);
} catch (e) {
logger.error('Error checking balances', e);
// 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);
}
}, 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<xERC20Limit>) {
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<ChainMap<xERC20Limit>> {
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<xERC20Limit> => {
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);

Loading…
Cancel
Save