Skip to content

improve: generic inventory accounting for L2-only equivalence-remapped tokens (pathUSD, USDH)#3073

Draft
nicholaspai wants to merge 23 commits intomasterfrom
pathusd-cumulative-balances
Draft

improve: generic inventory accounting for L2-only equivalence-remapped tokens (pathUSD, USDH)#3073
nicholaspai wants to merge 23 commits intomasterfrom
pathusd-cumulative-balances

Conversation

@nicholaspai
Copy link
Member

@nicholaspai nicholaspai commented Mar 20, 2026

Summary

Replaces hardcoded pathUSD and USDH handling across the codebase with generic resolution via TOKEN_EQUIVALENCE_REMAPPING from @across-protocol/constants.

New TokenUtils helpers (src/utils/TokenUtils.ts)

  • getInventoryEquivalentL1TokenAddress — resolves an L2-only token to its inventory-equivalent L1 token (e.g. pathUSD → USDC) via TOKEN_EQUIVALENCE_REMAPPING
  • getInventoryBalanceContributorTokens — discovers all L2 tokens that contribute to a given L1 token's balance on a chain (e.g. USDC on Tempo → [USDC.e, pathUSD])
  • isL2OnlyEquivalentToken — checks if a token symbol is an L2-only equivalence-remapped token

InventoryClient (src/clients/InventoryClient.ts)

  • getL1TokenAddress — replaced hardcoded ["pathUSD"] check with getInventoryEquivalentL1TokenAddress
  • getRemoteTokensForL1Token — non-alias config branch now uses getInventoryBalanceContributorTokens so pathUSD/USDH are included in getCumulativeBalance

TokenClient (src/clients/TokenClient.ts)

  • resolveRemoteTokens — uses getInventoryBalanceContributorTokens to discover tokens to fetch balances for, replacing hardcoded USDC.e/USDbC/USDzC/pathUSD list

Monitor (src/monitor/Monitor.ts)

  • pathUSD and USDH now appear as their own rows in the balance report (like USDC.e), discovered generically via getInventoryBalanceContributorTokens and isL2OnlyEquivalentToken
  • Pending refunds/withdrawals are correctly skipped for L2-only tokens (they share an L1 address with USDC and can't be refunded directly)
  • Removed L2_ONLY_TOKENS env var, L2Token interface, and all associated l2OnlyTokens logic — no longer needed

Other

  • BaseChainAdapter — replaced hardcoded pathUSD check with getInventoryEquivalentL1TokenAddress
  • Bumped @across-protocol/constants to ^3.1.102 (includes pathUSD and USDH in TOKEN_EQUIVALENCE_REMAPPING)

Test plan

  • TokenUtils unit tests verify L1 token resolution, contributor discovery, and L2-only detection
  • All 110 EVM tests pass (Relayer, InventoryClient, TokenClient, Monitor)
  • Live monitor run verified: pathUSD shows current balance on Tempo, USDH shows current balance on HyperEVM, no spurious pending balances
  • Live swapRebalancer run verified: cumulative USDC balance correctly includes pathUSD with patched constants

🤖 Generated with Claude Code

@nicholaspai nicholaspai marked this pull request as ready for review March 20, 2026 03:10
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f3cbe062b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

nicholaspai and others added 10 commits March 20, 2026 09:44
In aggregate balance mode, getCurrentAllocationPct reflects the combined
balance across all contributor tokens, but withdrawExcessBalances only
withdraws the canonical l2Token. Cap the withdrawal amount at the actual
l2Token balance to avoid submitting failing transactions when the excess
is mostly held in non-canonical contributor tokens (e.g. pathUSD).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update getL2ToL1TokenMap to discover L2 tokens via
TOKEN_EQUIVALENCE_REMAPPING in addition to hub chain address matching.
This makes pathUSD show up under the USDC row for Tempo in the relayer
balance report.

Also switch l2TokenAmountToL1TokenAmountConverter to use
getInventoryEquivalentL1TokenAddress so it can resolve tokens like
pathUSD that have no direct L1 mapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rows in monitor

- Bump @across-protocol/constants to ^3.1.101 which adds pathUSD to
  TOKEN_EQUIVALENCE_REMAPPING, enabling generic resolution without
  hardcoded mappings.
- Generalize monitor balance report to display L2-only equivalence-
  remapped tokens (pathUSD, USDH) as their own rows, matching how
  USDC.e is displayed, rather than folding them into the parent token.
- Fix RefundChain test: Tempo deposit was spreading uninitialized
  sampleDepositData (set in a sibling describe's beforeEach).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tokens

L2-only equivalence-remapped tokens (pathUSD, USDH) share the same L1
address as their parent (USDC). The monitor's updateLatestAndFutureRelayerRefunds
and updatePendingL2Withdrawals were querying by L1 address for each report
row, causing the same refunds/withdrawals to be counted under USDC, pathUSD,
and USDH rows simultaneously. Skip these child rows since their refunds
are already captured under the parent USDC row.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pathUSD and USDH were initialized with balance entries for every chain
in the report, showing zeroes on chains where they don't exist. Now
L2-only equivalence-remapped tokens are initialized and displayed only
for chains where they have an address in TOKEN_SYMBOLS_MAP, matching
the pattern used for L2_ONLY_TOKENS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add doc comments to getInventoryEquivalentL1TokenAddress and
  getInventoryBalanceContributorTokens in TokenUtils.
- Add isL2OnlyEquivalentToken helper to TokenUtils and use it in
  Monitor instead of raw TOKEN_EQUIVALENCE_REMAPPING lookups.
- Use getInventoryBalanceContributorTokens in Monitor's getL2ToL1TokenMap
  for consistent L2 token discovery.
- Simplify TokenClient catch to return empty array with original comment.
- Use getInventoryBalanceContributorTokens in InventoryClient's
  getRemoteTokensForL1Token for non-alias config to include L2-only
  equivalence-remapped tokens in cumulative balances.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: nicholaspai <9457025+nicholaspai@users.noreply.github.com>
nicholaspai and others added 4 commits March 20, 2026 16:12
L2-only tokens (pathUSD, USDH) are now discovered generically via
TOKEN_EQUIVALENCE_REMAPPING in getL1TokensForRelayerBalancesReport and
getL2ToL1TokenMap. The L2_ONLY_TOKENS env var, L2Token interface, and
all associated parsing/initialization/reporting logic are no longer
needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace direct TOKEN_EQUIVALENCE_REMAPPING iteration in
  getL1TokensForRelayerBalancesReport with getInventoryBalanceContributorTokens
  and isL2OnlyEquivalentToken.
- Simplify getL2ToL1TokenMap catch to return empty array instead of
  falling back to getRemoteTokenForL1Token which also fails for
  unknown tokens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nicholaspai
Copy link
Member Author

@codex

@nicholaspai nicholaspai changed the title fix: use TOKEN_EQUIVALENCE_REMAPPING for inventory balance contributors improve: generic inventory accounting for L2-only equivalence-remapped tokens (pathUSD, USDH) Mar 20, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

const l2Token = this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, chainId);
if (!isDefined(l2Token)) {

P1 Badge Handle executed refunds for remapped contributor tokens

After this commit, fills for equivalence-remapped assets like pathUSD/USDH are folded into their parent L1 token via getInventoryEquivalentL1TokenAddress(), but this boundary check still looks up only one HubPool-mapped l2Token when deciding whether a relayer-refund bundle has already executed. src/dataworker/DataworkerUtils.ts:253-303 builds refund leaves by the actual l2TokenAddress, including tokens that have no pool-rebalance route, so bundles that paid pathUSD/USDH refunds will not satisfy this check and the same fills keep being counted as “upcoming.” On chains using remapped tokens, that inflates BundleDataApproxClient refunds/deposits and can skew refund-chain selection and monitor pending-refund totals.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +496 to +499
for (const contributor of getInventoryBalanceContributorTokens(l1Token.address, chainId, hubChainId)) {
const { symbol } = getTokenInfo(contributor, chainId);
if (isL2OnlyEquivalentToken(symbol, hubChainId)) {
l2OnlySymbols.add(symbol);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve monitor rows configured through L2_ONLY_TOKENS

This new discovery path only keeps contributor symbols that satisfy isL2OnlyEquivalentToken(...), and the matching L2_ONLY_TOKENS parsing/update logic was removed from MonitorConfig/updateCurrentRelayerBalances(). If an existing monitor deployment uses L2_ONLY_TOKENS for any L2-only asset that is not in TOKEN_EQUIVALENCE_REMAPPING, reportRelayerBalances() will now silently drop that token entirely instead of continuing to report its balances.

Useful? React with 👍 / 👎.

@nicholaspai
Copy link
Member Author

💡 Codex Review

const l2Token = this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, chainId);
if (!isDefined(l2Token)) {

P1 Badge Handle executed refunds for remapped contributor tokens
After this commit, fills for equivalence-remapped assets like pathUSD/USDH are folded into their parent L1 token via getInventoryEquivalentL1TokenAddress(), but this boundary check still looks up only one HubPool-mapped l2Token when deciding whether a relayer-refund bundle has already executed. src/dataworker/DataworkerUtils.ts:253-303 builds refund leaves by the actual l2TokenAddress, including tokens that have no pool-rebalance route, so bundles that paid pathUSD/USDH refunds will not satisfy this check and the same fills keep being counted as “upcoming.” On chains using remapped tokens, that inflates BundleDataApproxClient refunds/deposits and can skew refund-chain selection and monitor pending-refund totals.

ℹ️ About Codex in GitHub

Is this still an issue for you assuming pathUSD and USDH cannot ever be refundTokens? The only token that can be a refund token is the one which has a pool rebalance route connection with the L1 token

@codex address this feedback

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f5c94511e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 232 to +234
protected getL1TokenAddress(l2Token: Address, chainId: number): Address | undefined {
try {
return getL1TokenAddress(l2Token, chainId);
return getInventoryEquivalentL1TokenAddress(l2Token, chainId, this.hubPoolClient.chainId);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve origin-chain refund matching for remapped tokens

This is still reachable when the remapped asset is the origin input token. depositForcesOriginChainRepayment() forces origin-chain repayment for tokens with no pool rebalance route (src/utils/FillUtils.ts:70-76), and _buildRelayerRefundRoot() then emits the refund leaf under that actual l2TokenAddress (src/dataworker/DataworkerUtils.ts:257-274). After this change, getL1TokenAddress() folds that fill into its parent L1 token, but getUnexecutedBundleStartBlocks() still considers a bundle executed only if it finds an execution for getL2TokenForL1TokenAtBlock(l1Token, chainId) (src/clients/BundleDataApproxClient.ts:145-160). For a pathUSD/USDH-origin refund, that lookup returns the canonical USDC token, not the executed pathUSD/USDH leaf, so the boundary never advances and the same refunds keep being counted as upcoming.

Useful? React with 👍 / 👎.

@nicholaspai nicholaspai marked this pull request as draft March 21, 2026 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant