Skip to content

feat(rebalancer): support withdrawing (W)GHO from Lens to Mainnet#3542

Open
droplet-rl wants to merge 3 commits into
masterfrom
droplet/T90K0AL22-C09KEFHB9JB-1783087319-701949
Open

feat(rebalancer): support withdrawing (W)GHO from Lens to Mainnet#3542
droplet-rl wants to merge 3 commits into
masterfrom
droplet/T90K0AL22-C09KEFHB9JB-1783087319-701949

Conversation

@droplet-rl

Copy link
Copy Markdown
Contributor

Summary

Adds the ability for the rebalancer (relayer inventory management) to pull (W)GHO on Lens back to Mainnet, mirroring the existing OP stack WETH L2→L1 withdrawal support and the Lens_SpokePool._bridgeTokensToHubPool() flow.

How it works

Lens' registered L1 base token is Mainnet WGHO (0x1ff1...b5a, verified via BridgeHub.baseToken(232)), and the pool rebalance route for Lens maps L1 WGHO ↔ Lens WGHO (0x6bDc...e2F). The withdrawal path is the same one the Lens SpokePool uses:

  1. Unwrap WGHO on Lens into native GHO (WETH9-style withdraw(uint256)).
  2. Withdraw via the L2BaseToken system contract (0x...800A): withdraw(_l1Receiver) with value = amount.
  3. Once the batch executes on L1, the zkSync finalizer finalizes the withdrawal against the L1Nullifier (existing legacy-bridge path for Lens), which releases Mainnet WGHO from the ZkStack NativeTokenVault to the recipient.

Changes

  • src/adapter/l2Bridges/ZKStackBaseTokenBridge.ts (new): L2→L1 bridge adapter modeled on OpStackWethBridge. constructWithdrawToL1Txns emits the unwrap + base-token withdrawal txns; getL2PendingWithdrawalAmount matches L2 Withdrawal events (L2BaseToken) against L1 BridgeMint events (NativeTokenVault, filtered by the WGHO assetId and receiver) for the withdrawal-volume rate limit. Pending-withdrawal lookback is 2 days + 1 hour.
  • src/common/Constants.ts: registers the adapter in CUSTOM_L2_BRIDGE[LENS] keyed by Mainnet WGHO, matching the SUPPORTED_TOKENS[LENS] "WGHO" entry (resolveAcrossToken("WGHO", MAINNET)).
  • src/finalizer/utils/zkSync.ts: the zkSync finalizer previously only discovered SpokePool withdrawals via TokensBridged. It now also queries L2BaseToken.Withdrawal events initiated by monitored sender addresses (relayer EOAs, FINALIZER_WITHDRAWAL_TO_ADDRESSES, etc., excluding the SpokePool) and feeds them through the existing proof/finalization flow — the zk-stack analog of the OP stack finalizer's manual-EOA-withdrawal discovery. No-op on chains without an l2BaseToken contract entry (only Lens today).
  • src/common/ContractAddresses.ts + src/common/abi/ZkStackL2BaseToken.json: l2BaseToken contract entry for Lens (withdraw(address) + Withdrawal event).

Activation

No behavior change until operators set withdrawExcessPeriod for WGHO on Lens in the inventory config (same activation switch as the OP stack chains).

Testing

  • New unit tests in test/generic-l2-adapters/ZKStackBaseToken.ts (withdrawal txn construction, pending-withdrawal accounting, sender/receiver filtering) with new mocks in contracts/MockZKSyncEvents.sol. All pass.
  • test/generic-l2-adapters/*, test/generic-adapters/zkSync.ts, test/InventoryClient.InventoryRebalance.ts, test/generic-adapters/AdapterManager.SendTokensCrossChain.ts all pass; full yarn lint and tsc --noEmit clean.
  • Verified on-chain: BridgeHub.baseToken(232) = 0x1ff1... (Mainnet WGHO) and NativeTokenVault.assetId(WGHO) != 0.
  • Suggest a small manual withdrawal on prod before enabling withdrawExcessPeriod, to confirm the zksync-ethers finalization params path end-to-end for base-token messages (it is the same L1Nullifier legacy finalizeWithdrawal path already used for Lens WETH/USDC withdrawals).

No README/AGENTS.md updates needed: the change plugs into existing extension points (CUSTOM_L2_BRIDGE registration, finalizer chain-family routing) without altering interfaces or configuration semantics.

🤖 Generated with Claude Code

Adds an L2 bridge adapter that lets the relayer's inventory management pull
excess WGHO on Lens back to Mainnet, mirroring the existing OP stack WETH
withdrawal support and the Lens_SpokePool's _bridgeTokensToHubPool() flow:
unwrap WGHO into native GHO, then withdraw via the L2BaseToken system
contract. The withdrawal finalizes on L1 by releasing Mainnet WGHO (Lens'
registered L1 base token) from the ZkStack NativeTokenVault.

Also extends the zkSync finalizer to discover base-token withdrawals
initiated directly by monitored EOAs (the SpokePool's own withdrawals are
already covered via TokensBridged events), so relayer-initiated withdrawals
get finalized on L1, mirroring the OP stack finalizer's handling of manual
EOA withdrawals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

Copy link
Copy Markdown

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: de38b71aee

ℹ️ 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 thread src/common/Constants.ts
Comment on lines +693 to +695
[CHAIN_IDs.LENS]: {
// (W)GHO is Lens' custom gas token, so it is withdrawn via the L2BaseToken system contract.
[TOKEN_SYMBOLS_MAP.WGHO.addresses[CHAIN_IDs.MAINNET]]: ZKStackBaseTokenBridge,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add docs for the Lens WGHO withdrawal route

This new CUSTOM_L2_BRIDGE[LENS] entry enables a new Lens WGHO L2→L1 withdrawal path and the commit also changes zkSync finalizer discovery, but the repo-level AGENTS.md documentation maintenance rule requires relevant README.md/AGENTS.md files to be updated whenever behavior, configuration, interfaces, or runtime flow changes. Please add a short note to the relevant module docs, e.g. the rebalancer/client docs, so operators can discover the new Lens base-token withdrawal flow and finalizer dependency.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a short "current behavior" note to src/clients/README.md under "Transferring Tokens Across Chains" (commit 3eb2535): it documents the CUSTOM_L2_BRIDGE registration, the Lens (W)GHO base-token withdrawal flow via the L2BaseToken system contract / NativeTokenVault, and the zkSync finalizer dependency for relayer-initiated (EOA) base-token withdrawals.

…inalizer dependency

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@droplet-rl

Copy link
Copy Markdown
Contributor Author

Addressed the Codex review feedback in commit 3eb2535: added a short note to src/clients/README.md (Inventory Client → "Transferring Tokens Across Chains") documenting the new Lens (W)GHO base-token withdrawal route — CUSTOM_L2_BRIDGE registration, unwrap + L2BaseToken system-contract withdrawal, L1 finalization via the ZkStack NativeTokenVault — and the zkSync finalizer's role in discovering and finalizing relayer-initiated (EOA) base-token withdrawals. Replied on the inline thread as well.

@codex review — docs-only follow-up addressing your feedback.

@droplet-rl

Copy link
Copy Markdown
Contributor Author

🔎 View trace

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

Copy link
Copy Markdown

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: de38b71aee

ℹ️ 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 thread src/finalizer/utils/zkSync.ts Outdated
amountToReturn: event.args._amount,
chainId,
leafId: 0,
l2TokenAddress: EvmAddress.from(address),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Use WGHO when reporting base-token withdrawals

When the zkSync finalizer discovers relayer-initiated Lens base-token withdrawals, this synthetic TokensBridged event records the L2BaseToken system contract (0x...800A) as l2TokenAddress. The finalizer later calls getTokenInfo(l2TokenAddress, l2ChainId) to build the CrossChainMessage, and on Lens that address resolves to native GHO, while this inventory route unwraps WGHO and releases Mainnet WGHO from the NativeTokenVault. In runs that finalize these withdrawals, logs/notifications will report the wrong asset, making WGHO inventory withdrawals look like unrelated GHO withdrawals; use the Lens WGHO token address here or otherwise override the reported symbol.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — confirmed: in the constants token map, GHO's Lens address is exactly the L2BaseToken system contract (0x...800A), so getTokenInfo() would have labelled these as GHO (which on Mainnet is an unrelated token) instead of the WGHO the route moves and releases from the NativeTokenVault. Fixed in 1b13507: the synthetic TokensBridged events now record the chain's wrapped base token (wrappedNativeToken contract entry, i.e. WGHO on Lens) as l2TokenAddress, matching what real SpokePool TokensBridged events carry. The address is only used for reporting and the USDC-custom-bridge checks, so finalization behavior is unchanged.

The synthetic TokensBridged events for EOA-initiated base-token withdrawals
recorded the L2BaseToken system contract as l2TokenAddress, which
getTokenInfo() resolves to the native gas token (GHO on Lens) rather than
the WGHO the route actually moves and releases on L1, mislabelling these
withdrawals in finalizer logs/notifications.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@droplet-rl

Copy link
Copy Markdown
Contributor Author

Addressed the second Codex review in commit 1b13507. The P3 finding was valid: the synthetic TokensBridged events for relayer-initiated base-token withdrawals recorded the L2BaseToken system contract (0x…800A) as l2TokenAddress, and since that address is also GHO's registered Lens address in the token map, getTokenInfo() would have reported these WGHO inventory withdrawals as GHO in finalizer logs/notifications. The finalizer now records the chain's wrapped base token (WGHO on Lens, via the wrappedNativeToken contract entry) instead, consistent with real SpokePool TokensBridged events. Finalization behavior is unchanged — the address only fed reporting and the USDC-custom-bridge checks. Typecheck, lint, and the ZkStack base-token adapter tests all pass.

@codex review — pushed a fix for the base-token withdrawal labelling.

@droplet-rl

Copy link
Copy Markdown
Contributor Author

🔎 View trace

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

Copy link
Copy Markdown

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: 3eb25358da

ℹ️ 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 thread src/finalizer/utils/zkSync.ts Outdated
amountToReturn: event.args._amount,
chainId,
leafId: 0,
l2TokenAddress: EvmAddress.from(address),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Label base-token withdrawals with WGHO

For Lens relayer-initiated WGHO withdrawals, this synthetic event represents the wrapped base token that was unwrapped before calling L2BaseToken.withdraw, but storing the l2BaseToken system-contract address makes the later getTokenInfo(l2TokenAddress, l2ChainId) path report the finalized withdrawal as native GHO/0x...800A instead of WGHO. This leaves finalizer logs/notifications mislabeled for these inventory withdrawals; use the chain's wrapped base-token address for the synthetic TokensBridged token field.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was already fixed in 1b13507 (this review ran against the older commit 3eb2535): getBaseTokenWithdrawalEvents now records the chain's wrapped base token (WGHO on Lens, via the wrappedNativeToken contract entry) as l2TokenAddress in the synthetic TokensBridged events, so getTokenInfo() reports the finalized withdrawals as WGHO. Same finding as #3542 (comment).

@droplet-rl

Copy link
Copy Markdown
Contributor Author

This review's single P3 comment is a duplicate of the previous review's finding — it ran against commit 3eb2535, which predates the fix. The base-token withdrawal labelling was already corrected in 1b13507 (synthetic TokensBridged events now carry the wrapped base token, WGHO on Lens, instead of the L2BaseToken system contract address). No code changes needed; I replied on the inline thread pointing to the fix.

@droplet-rl

Copy link
Copy Markdown
Contributor Author

🔎 View trace

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