Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(InventoryClient): Support 1:many HubPool mappings #1465

Merged
merged 98 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
110bcb1
refactor(RelayerConfig): Simplify InventoryConfig parsing
pxrl Apr 29, 2024
1dacc1e
Merge branch 'master' into pxrl/inventoryParsing
pxrl Apr 30, 2024
703f028
Additional simplification
pxrl Apr 30, 2024
b2fa39a
Drop
pxrl Apr 30, 2024
dd5e453
Lint & chill
pxrl Apr 30, 2024
9efd8fe
improve(RelayerConfig): Permit symbol-based token config
pxrl Apr 30, 2024
20da413
feat(InventoryClient): Support 1:many HubPool mappings
pxrl Apr 30, 2024
951a94d
Merge remote-tracking branch 'origin/master' into pxrl/usdcInventory
pxrl May 1, 2024
c57caef
Merge remote-tracking branch 'origin/master' into pxrl/usdcInventory
pxrl May 1, 2024
e3b6665
Update repayment shortfall check
pxrl May 1, 2024
ec623d6
Update
pxrl May 1, 2024
a48be1f
Update allocations & l2 token requirements
pxrl May 1, 2024
a65330a
Merge branch 'master' into pxrl/usdcInventory
pxrl May 1, 2024
56abc3c
WIP
pxrl May 1, 2024
cb5518b
wip...
pxrl May 1, 2024
ce2f5a1
Merge remote-tracking branch 'origin/master' into pxrl/usdcInventory
pxrl May 2, 2024
2e2f03b
Revert "wip..."
pxrl May 2, 2024
564e12e
Cleanup
pxrl May 2, 2024
d1d0f79
Typo
pxrl May 2, 2024
5159d12
Update getBalanceOnChain
pxrl May 2, 2024
8bef2e3
Migrate balance checker
pxrl May 2, 2024
2efc0f9
Fix
pxrl May 2, 2024
b064dee
Catch undefined destination tokens
pxrl May 2, 2024
f435d81
Fix test
pxrl May 3, 2024
33c3fac
lint
pxrl May 3, 2024
aefabe8
Fix test
pxrl May 3, 2024
73b1990
Merge branch 'master' into pxrl/usdcInventory
pxrl May 3, 2024
8c8abb6
Add comments
pxrl May 3, 2024
47fec54
Fix test
pxrl May 3, 2024
5a30873
Fix test
pxrl May 3, 2024
d26d1ac
Revert return
pxrl May 3, 2024
4f872bf
Rename type
pxrl May 3, 2024
26d8dfd
Initial tests
pxrl May 6, 2024
9f0def9
Fix tests
pxrl May 6, 2024
6fb41ea
Overhaul test
pxrl May 6, 2024
53811c5
Merge remote-tracking branch 'origin/master' into pxrl/usdcInventory
pxrl May 6, 2024
8d44904
Fix minor merge error
pxrl May 6, 2024
bd18e01
Tidy up
pxrl May 6, 2024
5b88f19
Fix balance check
pxrl May 6, 2024
0c40936
Sub in chain alias
pxrl May 6, 2024
5e80d91
Add l2Token
pxrl May 6, 2024
bca53b8
refactor(test): InventoryClient simplifications
pxrl May 6, 2024
520a284
Merge remote-tracking branch 'origin/pxrl/inventoryClientTest' into p…
pxrl May 6, 2024
1b89118
lint
pxrl May 6, 2024
97c5a3a
Merge remote-tracking branch 'origin/pxrl/inventoryClientTest' into p…
pxrl May 6, 2024
cb9db39
Simplify
pxrl May 6, 2024
90510b3
Merge remote-tracking branch 'origin/master' into pxrl/usdcInventory
pxrl May 6, 2024
eb11de6
refactor(CrossChainTransferClient): Support unique L2 tokens
pxrl May 1, 2024
85cab0a
lint
pxrl May 1, 2024
4da8064
Add InventoryClient shims
pxrl May 1, 2024
bb9343a
fix: resolve linea (#1472)
james-a-morris May 2, 2024
a1ccd0a
Merge branch 'master' into pxrl/usdcInventory
pxrl May 7, 2024
c73791d
Merge branch 'master' into pxrl/usdcInventory
pxrl May 7, 2024
baf3cbd
refactor(test): Cleanup InventoryClient refund chain test
pxrl May 7, 2024
f9c3bf1
lint
pxrl May 7, 2024
3f08e94
Merge branch 'master' into pxrl/inventoryClientTest
pxrl May 7, 2024
9aa7db0
Merge branch 'master' into pxrl/crossChainTransfer
james-a-morris May 7, 2024
9e05d1e
Merge branch 'pxrl/inventoryClientTest' into pxrl/usdcInventory
pxrl May 7, 2024
fc15beb
Initial repayment chain test
pxrl May 7, 2024
d04f442
Stragglers
pxrl May 7, 2024
e2535ec
Merge branch 'pxrl/inventoryClientTest' into pxrl/usdcInventory
pxrl May 7, 2024
06ede93
lint
pxrl May 7, 2024
e3da99e
Add rebalancing test
pxrl May 7, 2024
ea78310
Merge branch 'master' into pxrl/inventoryClientTest
pxrl May 7, 2024
bbd8fb9
Additional
pxrl May 7, 2024
6cb87f3
Tweak
pxrl May 7, 2024
ece343c
Merge branch 'pxrl/inventoryClientTest' into pxrl/usdcInventory
pxrl May 7, 2024
20ad610
Doc
pxrl May 7, 2024
2e1ce4c
Update method name & document
pxrl May 7, 2024
b1f2902
Merge branch 'master' into pxrl/crossChainTransfer
pxrl May 7, 2024
9eba919
Merge branch 'master' into pxrl/usdcInventory
pxrl May 8, 2024
4bdc030
Make l2Token required
pxrl May 8, 2024
4886cf2
Relocate instantiation
pxrl May 8, 2024
17c4d15
Merge branch 'master' into pxrl/crossChainTransfer
pxrl May 8, 2024
c17b614
lint
pxrl May 8, 2024
b9f0b4b
Identify todo
pxrl May 8, 2024
7fcbb42
improve(accounting): account for l2 routes on non-op-stack bridges (…
james-a-morris May 8, 2024
2b20472
Merge branch 'master' into pxrl/crossChainTransfer
james-a-morris May 8, 2024
4cce142
Merge branch 'master' into pxrl/crossChainTransfer
james-a-morris May 8, 2024
3fe67d4
Merge branch 'master' into pxrl/usdcInventory
pxrl May 8, 2024
a0836fe
Merge branch 'master' into pxrl/crossChainTransfer
pxrl May 8, 2024
eeb8641
chore: bump package
james-a-morris May 8, 2024
7d7c19f
Merge branch 'master' into pxrl/crossChainTransfer
pxrl May 8, 2024
6db6e60
Add missing l2Token arguments
pxrl May 9, 2024
4007994
Merge branch 'pxrl/crossChainTransfer' into pxrl/usdcInventory
pxrl May 9, 2024
d68728e
Update getBalanceOnChain
pxrl May 9, 2024
f518474
Fix test
pxrl May 9, 2024
d56505e
nit: lint
james-a-morris May 9, 2024
3d03d77
nit: optimize call
james-a-morris May 9, 2024
36e2287
nit: re-run test
james-a-morris May 9, 2024
16f04e6
Merge branch 'pxrl/crossChainTransfer' into pxrl/usdcInventory
pxrl May 9, 2024
0d759cb
lint
pxrl May 9, 2024
24cc6f6
nit: re-run test
james-a-morris May 9, 2024
7911dc1
fix(test): Avoid external RPC call
pxrl May 9, 2024
4f66a64
Merge branch 'pxrl/fixTest' into pxrl/crossChainTransfer
pxrl May 9, 2024
3c22411
Merge branch 'pxrl/crossChainTransfer' into pxrl/usdcInventory
pxrl May 9, 2024
901b217
Merge branch 'master' into pxrl/usdcInventory
pxrl May 9, 2024
69abda2
Merge branch 'master' into pxrl/usdcInventory
pxrl May 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 96 additions & 40 deletions src/clients/InventoryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
} from "../utils";
import { HubPoolClient, TokenClient, BundleDataClient } from ".";
import { AdapterManager, CrossChainTransferClient } from "./bridges";
import { InventoryConfig, V3Deposit } from "../interfaces";
import { V3Deposit } from "../interfaces";
import { InventoryConfig, isAliasConfig, TokenInventoryConfig } from "../interfaces/InventoryManagement";
import lodash from "lodash";
import { CONTRACT_ADDRESSES, SLOW_WITHDRAWAL_CHAINS } from "../common";
import { CombinedRefunds } from "../dataworker/DataworkerUtils";
Expand Down Expand Up @@ -68,6 +69,21 @@ export class InventoryClient {
this.formatWei = createFormatFunction(2, 4, false, 18);
}

getTokenConfig(hubPoolToken: string, chainId: number, spokePoolToken?: string): TokenInventoryConfig | undefined {
const tokenConfig = this.inventoryConfig.tokenConfig[hubPoolToken];
assert(isDefined(tokenConfig), `getTokenConfig: No token config found for ${hubPoolToken}.`);

if (isAliasConfig(tokenConfig)) {
assert(
isDefined(spokePoolToken),
`Cannot resolve ambiguous ${getNetworkName(chainId)} token config for ${hubPoolToken}`
);
return tokenConfig[spokePoolToken]?.[chainId];
} else {
return tokenConfig[chainId];
}
}

// Get the total balance across all chains, considering any outstanding cross chain transfers as a virtual balance on that chain.
getCumulativeBalance(l1Token: string): BigNumber {
return this.getEnabledChains()
Expand All @@ -83,9 +99,12 @@ export class InventoryClient {
return bnZero;
}

const balances = this.getDestinationTokensForL1Token(l1Token, chainId).map(
(token) => this.tokenClient.getBalance(Number(chainId), token) || bnZero
pxrl marked this conversation as resolved.
Show resolved Hide resolved
);

// If the chain does not have this token (EG BOBA on Optimism) then 0.
const balance =
this.tokenClient.getBalance(chainId, this.getDestinationTokenForL1Token(l1Token, chainId)) || bnZero;
const balance = balances.reduce((acc, curr) => acc.add(curr), bnZero);

// Consider any L1->L2 transfers that are currently pending in the canonical bridge.
return balance.add(
Expand Down Expand Up @@ -134,13 +153,25 @@ export class InventoryClient {

// Find how short a given chain is for a desired L1Token.
getTokenShortFall(l1Token: string, chainId: number): BigNumber {
return this.tokenClient.getShortfallTotalRequirement(chainId, this.getDestinationTokenForL1Token(l1Token, chainId));
return this.getDestinationTokensForL1Token(l1Token, chainId)
.map((token) => this.tokenClient.getShortfallTotalRequirement(chainId, token))
.reduce((acc, curr) => acc.add(curr), bnZero);
}
pxrl marked this conversation as resolved.
Show resolved Hide resolved

getDestinationTokenForL1Token(l1Token: string, chainId: number | string): string {
getRepaymentTokenForL1Token(l1Token: string, chainId: number | string): string {
return this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, Number(chainId));
}

getDestinationTokensForL1Token(l1Token: string, chainId: number | string): string[] {
pxrl marked this conversation as resolved.
Show resolved Hide resolved
const tokenConfig = this.inventoryConfig.tokenConfig[l1Token];

if (isAliasConfig(tokenConfig)) {
return Object.keys(tokenConfig).filter((k) => isDefined(tokenConfig[k][chainId]));
}

return [this.getRepaymentTokenForL1Token(l1Token, chainId)];
}

getEnabledChains(): number[] {
return this.chainIdList;
}
Expand Down Expand Up @@ -213,9 +244,9 @@ export class InventoryClient {
const totalRefundsPerChain = this.getEnabledChains().reduce(
(refunds: { [chainId: string]: BigNumber }, chainId) => {
if (!this.hubPoolClient.l2TokenEnabledForL1Token(l1Token, chainId)) {
refunds[chainId] = toBN(0);
refunds[chainId] = bnZero;
} else {
const destinationToken = this.getDestinationTokenForL1Token(l1Token, chainId);
const destinationToken = this.getRepaymentTokenForL1Token(l1Token, chainId);
refunds[chainId] = this.bundleDataClient.getTotalRefund(
refundsToConsider,
this.relayer,
Expand Down Expand Up @@ -293,8 +324,8 @@ export class InventoryClient {
` (${inputToken} != ${outputToken})`
);
}

l1Token ??= this.hubPoolClient.getL1TokenForL2TokenAtBlock(inputToken, originChainId);
const tokenConfig = this.inventoryConfig?.tokenConfig?.[l1Token];

// Consider any refunds from executed and to-be executed bundles. If bundle data client doesn't return in
// time, return an object with zero refunds for all chains.
Expand Down Expand Up @@ -350,7 +381,12 @@ export class InventoryClient {
for (const _chain of chainsToEvaluate) {
assert(this._l1TokenEnabledForChain(l1Token, _chain), `Token ${l1Token} not enabled for chain ${_chain}`);
// Destination chain:

// @todo: Determine whether this should be reduced over all repayment token balances, or only the repayment token.
// Per current implementation, it returns the shortfall for the sum of all destination tokens that map to l1Token,
// not necessarily just the repayment token.
const chainShortfall = this.getTokenShortFall(l1Token, _chain);

const chainVirtualBalance = this.getBalanceOnChainForL1Token(_chain, l1Token);
const chainVirtualBalanceWithShortfall = chainVirtualBalance.sub(chainShortfall);
let cumulativeVirtualBalanceWithShortfall = cumulativeVirtualBalance.sub(chainShortfall);
Expand All @@ -376,8 +412,10 @@ export class InventoryClient {
.div(cumulativeVirtualBalanceWithShortfallPostRelay);

// Consider configured buffer for target to allow relayer to support slight overages.
const thresholdPct = toBN(this.inventoryConfig.tokenConfig[l1Token][_chain].targetPct)
.mul(tokenConfig[_chain].targetOverageBuffer ?? toBNWei("1"))
const tokenConfig = this.getTokenConfig(l1Token, destinationChainId, outputToken);
assert(isDefined(tokenConfig), `No tokenConfig for ${l1Token} on ${_chain}.`);
const thresholdPct = toBN(tokenConfig.targetPct)
.mul(tokenConfig.targetOverageBuffer ?? toBNWei("1"))
.div(fixedPointAdjustment);
this.log(
`Evaluated taking repayment on ${
Expand Down Expand Up @@ -447,12 +485,13 @@ export class InventoryClient {
} else {
runningBalanceForToken = leaf.runningBalances[l1TokenIndex];
}

// Approximate latest running balance as last known proposed running balance...
// - minus total deposit amount on chain since the latest end block proposed
// - plus total refund amount on chain since the latest end block proposed
const upcomingDeposits = this.bundleDataClient.getUpcomingDepositAmount(
chainId,
this.getDestinationTokenForL1Token(l1Token, chainId),
this.getRepaymentTokenForL1Token(l1Token, chainId),
blockRange[1]
);
// Grab refunds that are not included in any bundle proposed on-chain. These are refunds that have not
Expand All @@ -462,7 +501,7 @@ export class InventoryClient {
// If a chain didn't exist in the last bundle or a spoke pool client isn't defined, then
// one of the refund entries for a chain can be undefined.
const upcomingRefundForChain = Object.values(
upcomingRefunds?.[chainId]?.[this.getDestinationTokenForL1Token(l1Token, chainId)] ?? {}
upcomingRefunds?.[chainId]?.[this.getRepaymentTokenForL1Token(l1Token, chainId)] ?? {}
).reduce((acc, curr) => acc.add(curr), bnZero);

// Updated running balance is last known running balance minus deposits plus upcoming refunds.
Expand Down Expand Up @@ -588,24 +627,28 @@ export class InventoryClient {
continue;
}

const currentAllocPct = this.getCurrentAllocationPct(l1Token, chainId);
const { thresholdPct, targetPct } = this.inventoryConfig.tokenConfig[l1Token][chainId];
if (currentAllocPct.lt(thresholdPct)) {
const deltaPct = targetPct.sub(currentAllocPct);
const amount = deltaPct.mul(cumulativeBalance).div(this.scalar);
const balance = this.tokenClient.getBalance(1, l1Token);
// Divide by scalar because allocation percent was multiplied by it to avoid rounding errors.
rebalancesRequired.push({
chainId,
l1Token,
currentAllocPct,
thresholdPct,
targetPct,
balance,
cumulativeBalance,
amount,
});
}
const l2Tokens = this.getDestinationTokensForL1Token(l1Token, chainId);
l2Tokens.forEach((l2Token) => {
const currentAllocPct = this.getCurrentAllocationPct(l1Token, chainId);
const tokenConfig = this.getTokenConfig(l1Token, chainId, l2Token);
const { thresholdPct, targetPct } = tokenConfig;
if (currentAllocPct.lt(thresholdPct)) {
const deltaPct = targetPct.sub(currentAllocPct);
const amount = deltaPct.mul(cumulativeBalance).div(this.scalar);
const balance = this.tokenClient.getBalance(this.hubPoolClient.chainId, l1Token);
// Divide by scalar because allocation percent was multiplied by it to avoid rounding errors.
rebalancesRequired.push({
chainId,
l1Token,
currentAllocPct,
thresholdPct,
targetPct,
balance,
cumulativeBalance,
amount,
});
}
});
}
}
return rebalancesRequired;
Expand Down Expand Up @@ -760,6 +803,10 @@ export class InventoryClient {
}

async unwrapWeth(): Promise<void> {
if (!this.isInventoryManagementEnabled()) {
return;
}

// Note: these types are just used inside this method, so they are declared in-line.
type ChainInfo = {
chainId: number;
Expand All @@ -775,16 +822,14 @@ export class InventoryClient {
const executedTransactions: ExecutedUnwrap[] = [];

try {
if (!this.isInventoryManagementEnabled()) {
return;
}
const l1Weth = TOKEN_SYMBOLS_MAP.WETH.addresses[this.hubPoolClient.chainId];
const chains = await Promise.all(
this.getEnabledChains()
.map((chainId) => {
const unwrapWethThreshold =
this.inventoryConfig.tokenConfig?.[l1Weth]?.[chainId.toString()]?.unwrapWethThreshold;
const unwrapWethTarget = this.inventoryConfig.tokenConfig?.[l1Weth]?.[chainId.toString()]?.unwrapWethTarget;
const tokenConfig = this.getTokenConfig(l1Weth, chainId);
assert(isDefined(tokenConfig));

const { unwrapWethThreshold, unwrapWethTarget } = tokenConfig;

// Ignore chains where ETH isn't the native gas token. Returning null will result in these being filtered.
if (chainId === CHAIN_IDs.POLYGON || unwrapWethThreshold === undefined || unwrapWethTarget === undefined) {
Expand All @@ -807,7 +852,7 @@ export class InventoryClient {

chains.forEach((chainInfo) => {
const { chainId, unwrapWethThreshold, unwrapWethTarget, balance } = chainInfo;
const l2WethBalance = this.tokenClient.getBalance(chainId, this.getDestinationTokenForL1Token(l1Weth, chainId));
const l2WethBalance = this.tokenClient.getBalance(chainId, this.getRepaymentTokenForL1Token(l1Weth, chainId));

if (balance.lt(unwrapWethThreshold)) {
const amountToUnwrap = unwrapWethTarget.sub(balance);
Expand Down Expand Up @@ -836,7 +881,7 @@ export class InventoryClient {
// is already complex logic and most of the time we'll not be sending batches of rebalance transactions.
for (const { chainInfo, amount } of unwrapsRequired) {
const { chainId } = chainInfo;
const l2Weth = this.getDestinationTokenForL1Token(l1Weth, chainId);
const l2Weth = this.getRepaymentTokenForL1Token(l1Weth, chainId);
this.tokenClient.decrementLocalBalance(chainId, l2Weth, amount);
const receipt = await this._unwrapWeth(chainId, l2Weth, amount);
executedTransactions.push({ chainInfo, amount, hash: receipt.hash });
Expand Down Expand Up @@ -865,7 +910,7 @@ export class InventoryClient {
"- WETH unwrap blocked. Required to send " +
`${formatter(amount.toString())} but relayer has ` +
`${formatter(
this.tokenClient.getBalance(chainId, this.getDestinationTokenForL1Token(l1Weth, chainId)).toString()
this.tokenClient.getBalance(chainId, this.getRepaymentTokenForL1Token(l1Weth, chainId)).toString()
)} WETH balance.\n`;
}

Expand Down Expand Up @@ -988,7 +1033,18 @@ export class InventoryClient {
}

_l1TokenEnabledForChain(l1Token: string, chainId: number): boolean {
return this.inventoryConfig.tokenConfig?.[l1Token]?.[String(chainId)] !== undefined;
const tokenConfig = this.inventoryConfig?.tokenConfig?.[l1Token];
if (!isDefined(tokenConfig)) {
return false;
}

// If tokenConfig directly references chainId, token is enabled.
if (!isAliasConfig(tokenConfig) && isDefined(tokenConfig[chainId])) {
return true;
}

// If any of the mapped symbols reference chainId, token is enabled.
return Object.keys(tokenConfig).some((symbol) => isDefined(tokenConfig[symbol][chainId]));
}

/**
Expand Down
43 changes: 28 additions & 15 deletions src/interfaces/InventoryManagement.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { BigNumber } from "ethers";
import { BigNumber, utils as ethersUtils } from "ethers";
import { TOKEN_SYMBOLS_MAP } from "../utils";

export type TokenInventoryConfig = {
targetOverageBuffer: BigNumber; // Max multiplier for targetPct, to give flexibility in repayment chain selection.
targetPct: BigNumber; // The desired amount of the given token on the L2 chainId.
thresholdPct: BigNumber; // Threshold, below which, we will execute a rebalance.
unwrapWethThreshold?: BigNumber; // Threshold for ETH to trigger WETH unwrapping to maintain ETH balance.
unwrapWethTarget?: BigNumber; // Amount of WETH to unwrap to refill ETH. Unused if unwrapWethThreshold is undefined.
};

export type ChainTokenConfig = {
[chainId: string]: TokenInventoryConfig;
};

// AliasConfig permits a single HubPool token to map onto multiple tokens on a remote chain.
export type ChainTokenInventory = {
[symbol: string]: ChainTokenConfig;
};

export interface InventoryConfig {
tokenConfig: {
[l1Token: string]: {
[chainId: string]: {
targetOverageBuffer: BigNumber; // The relayer will be allowed to hold this multiple times the targetPct
// of the full token balance on this chain.
targetPct: BigNumber; // The desired amount of the given token on the L2 chainId.
thresholdPct: BigNumber; // Threshold, below which, we will execute a rebalance.
unwrapWethThreshold?: BigNumber; // Threshold for ETH on this chain to trigger WETH unwrapping to maintain
// ETH balance
unwrapWethTarget?: BigNumber; // Amount of WETH to unwrap to refill ETH balance. Unused if unwrapWethThreshold
// is undefined.
};
};
};
// tokenConfig can map to a single token allocation, or a set of allocations that all map to the same HubPool token.
tokenConfig: { [l1Token: string]: ChainTokenConfig } | { [l1Token: string]: ChainTokenInventory };

nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
// If ETH balance on chain is above threshold, wrap the excess over the target to WETH.
wrapEtherTargetPerChain: {
[chainId: number]: BigNumber;
Expand All @@ -25,3 +32,9 @@ export interface InventoryConfig {
};
wrapEtherThreshold: BigNumber;
}

export function isAliasConfig(config: ChainTokenConfig | ChainTokenInventory): config is ChainTokenInventory {
return (
pxrl marked this conversation as resolved.
Show resolved Hide resolved
Object.keys(config).every((k) => ethersUtils.isAddress(k)) || Object.keys(config).every((k) => TOKEN_SYMBOLS_MAP[k])
);
}
Loading
Loading