feat(rebalancer): support withdrawing (W)GHO from Lens to Mainnet#3542
feat(rebalancer): support withdrawing (W)GHO from Lens to Mainnet#3542droplet-rl wants to merge 3 commits into
Conversation
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>
There was a problem hiding this comment.
💡 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".
| [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, |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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>
|
Addressed the Codex review feedback in commit 3eb2535: added a short note to @codex review — docs-only follow-up addressing your feedback. |
There was a problem hiding this comment.
💡 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".
| amountToReturn: event.args._amount, | ||
| chainId, | ||
| leafId: 0, | ||
| l2TokenAddress: EvmAddress.from(address), |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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>
|
Addressed the second Codex review in commit 1b13507. The P3 finding was valid: the synthetic @codex review — pushed a fix for the base-token withdrawal labelling. |
There was a problem hiding this comment.
💡 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".
| amountToReturn: event.args._amount, | ||
| chainId, | ||
| leafId: 0, | ||
| l2TokenAddress: EvmAddress.from(address), |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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).
|
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 |
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 viaBridgeHub.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:withdraw(uint256)).L2BaseTokensystem contract (0x...800A):withdraw(_l1Receiver)withvalue = amount.NativeTokenVaultto the recipient.Changes
src/adapter/l2Bridges/ZKStackBaseTokenBridge.ts(new): L2→L1 bridge adapter modeled onOpStackWethBridge.constructWithdrawToL1Txnsemits the unwrap + base-token withdrawal txns;getL2PendingWithdrawalAmountmatches L2Withdrawalevents (L2BaseToken) against L1BridgeMintevents (NativeTokenVault, filtered by the WGHOassetIdand receiver) for the withdrawal-volume rate limit. Pending-withdrawal lookback is 2 days + 1 hour.src/common/Constants.ts: registers the adapter inCUSTOM_L2_BRIDGE[LENS]keyed by Mainnet WGHO, matching theSUPPORTED_TOKENS[LENS]"WGHO" entry (resolveAcrossToken("WGHO", MAINNET)).src/finalizer/utils/zkSync.ts: the zkSync finalizer previously only discovered SpokePool withdrawals viaTokensBridged. It now also queriesL2BaseToken.Withdrawalevents 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 anl2BaseTokencontract entry (only Lens today).src/common/ContractAddresses.ts+src/common/abi/ZkStackL2BaseToken.json:l2BaseTokencontract entry for Lens (withdraw(address)+Withdrawalevent).Activation
No behavior change until operators set
withdrawExcessPeriodfor WGHO on Lens in the inventory config (same activation switch as the OP stack chains).Testing
test/generic-l2-adapters/ZKStackBaseToken.ts(withdrawal txn construction, pending-withdrawal accounting, sender/receiver filtering) with new mocks incontracts/MockZKSyncEvents.sol. All pass.test/generic-l2-adapters/*,test/generic-adapters/zkSync.ts,test/InventoryClient.InventoryRebalance.ts,test/generic-adapters/AdapterManager.SendTokensCrossChain.tsall pass; fullyarn lintandtsc --noEmitclean.BridgeHub.baseToken(232) = 0x1ff1...(Mainnet WGHO) andNativeTokenVault.assetId(WGHO) != 0.withdrawExcessPeriod, to confirm the zksync-ethers finalization params path end-to-end for base-token messages (it is the same L1Nullifier legacyfinalizeWithdrawalpath already used for Lens WETH/USDC withdrawals).No README/AGENTS.md updates needed: the change plugs into existing extension points (
CUSTOM_L2_BRIDGEregistration, finalizer chain-family routing) without altering interfaces or configuration semantics.🤖 Generated with Claude Code