@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
234 lines • 11.1 kB
JavaScript
import { IXERC20Lockbox__factory } from '@hyperlane-xyz/core';
import { assert, normalizeAddress, rootLogger, } from '@hyperlane-xyz/utils';
import { HyperlaneModule, } from '../core/AbstractHyperlaneModule.js';
import { MultiProtocolProvider } from '../providers/MultiProtocolProvider.js';
import { EvmXERC20Reader, limitsAreZero, limitsMatch, } from './EvmXERC20Reader.js';
import { EvmXERC20Adapter, EvmXERC20VSAdapter, } from './adapters/EvmTokenAdapter.js';
import { TokenType } from './config.js';
import { XERC20Type } from './types.js';
/**
* Module for managing XERC20 mint/burn limits and bridges.
* Follows HyperlaneModule pattern with read() and update() methods.
* Supports both Standard XERC20 (setLimits) and Velodrome XERC20 (setBufferCap/addBridge/removeBridge).
*/
export class EvmXERC20Module extends HyperlaneModule {
multiProvider;
logger = rootLogger.child({ module: 'EvmXERC20Module' });
reader;
multiProtocolProvider;
chainName;
constructor(multiProvider, args) {
super(args);
this.multiProvider = multiProvider;
this.chainName = this.multiProvider.getChainName(args.chain);
this.reader = new EvmXERC20Reader(multiProvider, args.chain);
this.multiProtocolProvider =
MultiProtocolProvider.fromMultiProvider(multiProvider);
}
async read() {
const type = await this.reader.deriveXERC20TokenType(this.args.addresses.xERC20);
let bridges = this.getExpectedBridges();
if (type === XERC20Type.Velo) {
const onChainBridges = await this.reader.readOnChainBridges(this.args.addresses.xERC20, type);
bridges = [...new Set([...bridges, ...onChainBridges])];
}
const limits = await this.reader.readLimits(this.args.addresses.xERC20, bridges, type);
return { type, limits };
}
/**
* Generate transactions to update XERC20 limits to match expected config.
* Detects drift and generates correction transactions.
*/
async update(expectedConfig) {
const actualConfig = await this.read();
assert(expectedConfig.type === actualConfig.type, `XERC20 type mismatch: expected ${expectedConfig.type} but on-chain is ${actualConfig.type}`);
const transactions = [];
const { missingBridges, extraBridges, limitMismatches } = this.detectDriftFromConfigs(expectedConfig, actualConfig);
for (const bridge of missingBridges) {
const limits = expectedConfig.limits[bridge];
if (limits) {
const txs = await this.generateAddBridgeTxs(bridge, limits);
transactions.push(...txs);
}
}
for (const { bridge, expected } of limitMismatches) {
const txs = await this.generateSetLimitsTxs(bridge, expected);
transactions.push(...txs);
}
if (expectedConfig.type === XERC20Type.Velo) {
for (const bridge of extraBridges) {
const txs = await this.generateRemoveBridgeTxs(bridge);
transactions.push(...txs);
}
}
if (transactions.length > 0) {
this.logger.info(`Generated ${transactions.length} XERC20 correction txs: ` +
`${missingBridges.length} missing, ${limitMismatches.length} mismatches, ${extraBridges.length} extra`);
}
return transactions;
}
/**
* Detect drift between expected and actual configurations.
*/
detectDriftFromConfigs(expected, actual) {
const missingBridges = [];
const limitMismatches = [];
const expectedBridges = Object.keys(expected.limits).map((addr) => normalizeAddress(addr));
const expectedBridgesSet = new Set(expectedBridges);
for (const [bridge, expectedLimits] of Object.entries(expected.limits)) {
const normalizedBridge = normalizeAddress(bridge);
const actualLimits = actual.limits[normalizedBridge] ?? actual.limits[bridge];
if (!actualLimits || limitsAreZero(actualLimits)) {
missingBridges.push(bridge);
continue;
}
if (!limitsMatch(expectedLimits, actualLimits)) {
limitMismatches.push({
bridge,
expected: expectedLimits,
actual: actualLimits,
});
}
}
let extraBridges = [];
if (expected.type === XERC20Type.Velo) {
extraBridges = Object.keys(actual.limits)
.filter((addr) => {
const normalized = normalizeAddress(addr);
return (!expectedBridgesSet.has(normalized) &&
!limitsAreZero(actual.limits[addr]));
})
.map((addr) => normalizeAddress(addr));
}
return { missingBridges, extraBridges, limitMismatches };
}
/**
* Get expected bridge addresses from config.
*/
getExpectedBridges() {
return Object.keys(this.args.config.limits);
}
/**
* Generate transactions to set limits for a bridge.
*/
async generateSetLimitsTxs(bridge, limits) {
const xERC20Address = this.args.addresses.xERC20;
const chainId = this.multiProvider.getEvmChainId(this.chainName);
const transactions = [];
if (limits.type === XERC20Type.Standard) {
const adapter = new EvmXERC20Adapter(this.chainName, this.multiProtocolProvider, { token: xERC20Address });
const tx = await adapter.populateSetLimitsTx(bridge, BigInt(limits.mint), BigInt(limits.burn));
transactions.push(this.annotateTransaction(tx, chainId, xERC20Address));
}
else {
const adapter = new EvmXERC20VSAdapter(this.chainName, this.multiProtocolProvider, { token: xERC20Address });
const bufferCapTx = await adapter.populateSetBufferCapTx(bridge, BigInt(limits.bufferCap));
transactions.push(this.annotateTransaction(bufferCapTx, chainId, xERC20Address));
const rateLimitTx = await adapter.populateSetRateLimitPerSecondTx(bridge, BigInt(limits.rateLimitPerSecond));
transactions.push(this.annotateTransaction(rateLimitTx, chainId, xERC20Address));
}
return transactions;
}
/**
* Generate transactions to add a bridge.
* For Standard XERC20, equivalent to setLimits.
* For Velodrome, uses addBridge function.
*/
async generateAddBridgeTxs(bridge, limits) {
if (limits.type === XERC20Type.Standard) {
return this.generateSetLimitsTxs(bridge, limits);
}
const xERC20Address = this.args.addresses.xERC20;
const chainId = this.multiProvider.getEvmChainId(this.chainName);
const adapter = new EvmXERC20VSAdapter(this.chainName, this.multiProtocolProvider, { token: xERC20Address });
const tx = await adapter.populateAddBridgeTx(BigInt(limits.bufferCap), BigInt(limits.rateLimitPerSecond), bridge);
return [this.annotateTransaction(tx, chainId, xERC20Address)];
}
/**
* Generate transactions to remove a bridge (Velodrome only).
*/
async generateRemoveBridgeTxs(bridge) {
const xERC20Address = this.args.addresses.xERC20;
const chainId = this.multiProvider.getEvmChainId(this.chainName);
const adapter = new EvmXERC20VSAdapter(this.chainName, this.multiProtocolProvider, { token: xERC20Address });
const tx = await adapter.populateRemoveBridgeTx(bridge);
return [this.annotateTransaction(tx, chainId, xERC20Address)];
}
annotateTransaction(tx, chainId, to) {
return {
...tx,
chainId,
to,
annotation: `XERC20 limit update for ${to}`,
};
}
static async fromWarpRouteConfig(multiProvider, chain, warpRouteConfig, warpRouteAddress) {
assert(warpRouteConfig.type === TokenType.XERC20 ||
warpRouteConfig.type === TokenType.XERC20Lockbox, `Expected XERC20 or XERC20Lockbox token type, got ${warpRouteConfig.type}`);
let xERC20Address = warpRouteConfig.token;
if (warpRouteConfig.type === TokenType.XERC20Lockbox) {
const provider = multiProvider.getProvider(chain);
const lockbox = IXERC20Lockbox__factory.connect(warpRouteConfig.token, provider);
xERC20Address = await lockbox.callStatic.XERC20();
}
const limits = {};
const xERC20Config = warpRouteConfig.xERC20;
const warpRouteLimits = xERC20Config?.warpRouteLimits;
if (warpRouteLimits) {
if (warpRouteLimits.type === XERC20Type.Standard) {
if (warpRouteLimits.mint != null && warpRouteLimits.burn != null) {
limits[warpRouteAddress] = {
type: XERC20Type.Standard,
mint: warpRouteLimits.mint,
burn: warpRouteLimits.burn,
};
}
}
else if (warpRouteLimits.type === XERC20Type.Velo) {
if (warpRouteLimits.bufferCap != null &&
warpRouteLimits.rateLimitPerSecond != null) {
limits[warpRouteAddress] = {
type: XERC20Type.Velo,
bufferCap: warpRouteLimits.bufferCap,
rateLimitPerSecond: warpRouteLimits.rateLimitPerSecond,
};
}
}
}
if (xERC20Config?.extraBridges) {
for (const extraBridge of xERC20Config.extraBridges) {
const { lockbox, limits: bridgeLimits } = extraBridge;
if (bridgeLimits.type === XERC20Type.Standard) {
if (bridgeLimits.mint != null && bridgeLimits.burn != null) {
limits[lockbox] = {
type: XERC20Type.Standard,
mint: bridgeLimits.mint,
burn: bridgeLimits.burn,
};
}
}
else if (bridgeLimits.type === XERC20Type.Velo) {
if (bridgeLimits.bufferCap != null &&
bridgeLimits.rateLimitPerSecond != null) {
limits[lockbox] = {
type: XERC20Type.Velo,
bufferCap: bridgeLimits.bufferCap,
rateLimitPerSecond: bridgeLimits.rateLimitPerSecond,
};
}
}
}
}
const type = warpRouteLimits?.type
? warpRouteLimits.type
: await new EvmXERC20Reader(multiProvider, chain).deriveXERC20TokenType(xERC20Address);
const config = { type, limits };
const module = new EvmXERC20Module(multiProvider, {
addresses: { xERC20: xERC20Address, warpRoute: warpRouteAddress },
chain,
config,
});
return { module, config };
}
}
//# sourceMappingURL=EvmXERC20Module.js.map