Skip to content

chore: Deploy Lens SpokePool & Adapter #978

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

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d68d1f7
feat: Linea CCTP V2 deployments (#947)
nicholaspai Apr 8, 2025
2727922
improve: query shared bridge address dynamically (#944)
bmzig Apr 8, 2025
0dcf7ef
Revert "feat: Linea CCTP V2 deployments (#947)" (#949)
nicholaspai Apr 8, 2025
77761d7
feat: Support Circle bridged & native USDC on zkSync (#941)
pxrl Apr 8, 2025
f6f8807
fix(ZkSync_SpokePool): Add missing bridged USDC approval (#967)
pxrl Apr 25, 2025
4c78547
[N-01]: Fix missing and misleading documentation (#973)
pxrl Apr 25, 2025
b251c10
L-01 Custom Gas Tokens Can Get Stuck In `HubPool` (#975)
bmzig Apr 28, 2025
f3887a5
N-02 Relay Tokens From L1 Emits Empty Transaction Hash When Relaying …
bmzig Apr 28, 2025
a84bc4a
chore: Deploy Lens SpokePool & Adapter
pxrl Apr 28, 2025
6db3e38
fix(adapter): Approve sharedBridge for custom gas token
pxrl Apr 28, 2025
004c39d
Merge remote-tracking branch 'origin/pxrl/fixApproval' into pxrl/depl…
pxrl Apr 29, 2025
9a9d5b0
Redeploy Lens adapter
pxrl Apr 29, 2025
f5e1c22
chore(ZkStack_Adapter): Remove unused canonicalTxHash (#982)
pxrl Apr 30, 2025
9bbfa05
fix(adapter): Approve sharedBridge for custom gas token (#981)
pxrl Apr 30, 2025
fee1e8a
Merge remote-tracking branch 'origin/audit-zkstack-usdc' into pxrl/de…
pxrl Apr 30, 2025
7abf165
Merge remote-tracking branch 'origin/master' into audit-zkstack-usdc
pxrl Apr 30, 2025
cd3524b
Cleanup merge
pxrl Apr 30, 2025
d8518da
Merge remote-tracking branch 'origin/audit-zkstack-usdc' into pxrl/de…
pxrl Apr 30, 2025
089ef01
Redeploy
pxrl Apr 30, 2025
17aae56
Back out yarn.lock change
pxrl Apr 30, 2025
a266283
Merge branch 'master' into pxrl/deployLens
pxrl May 2, 2025
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
6 changes: 3 additions & 3 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ jobs:
- name: Install Cargo toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
toolchain: nightly-2025-04-01
profile: minimal
components: rustc
- name: Install packages
run: yarn install --frozen-lockfile && rustup component add --toolchain nightly-x86_64-unknown-linux-gnu rustfmt
run: yarn install --frozen-lockfile && rustup component add --toolchain nightly-2025-04-01-x86_64-unknown-linux-gnu rustfmt
- name: Lint js
shell: bash
run: yarn lint-js
Expand Down Expand Up @@ -69,7 +69,7 @@ jobs:
- name: Install Cargo toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
toolchain: nightly-2025-04-01
profile: minimal
components: rustc
- name: Cache Cargo dependencies
Expand Down
14 changes: 13 additions & 1 deletion contracts/Lens_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,19 @@ contract Lens_SpokePool is ZkSync_SpokePool {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(
address _wrappedNativeTokenAddress,
IERC20 _circleUSDC,
ZkBridgeLike _zkUSDCBridge,
ITokenMessenger _cctpTokenMessenger,
uint32 _depositQuoteTimeBuffer,
uint32 _fillDeadlineBuffer
) ZkSync_SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer) {} // solhint-disable-line no-empty-blocks
)
ZkSync_SpokePool(
_wrappedNativeTokenAddress,
_circleUSDC,
_zkUSDCBridge,
_cctpTokenMessenger,
_depositQuoteTimeBuffer,
_fillDeadlineBuffer
)
{}
}
62 changes: 58 additions & 4 deletions contracts/ZkSync_SpokePool.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import "./SpokePool.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { CircleCCTPAdapter, CircleDomainIds, ITokenMessenger } from "./libraries/CircleCCTPAdapter.sol";
import { CrossDomainAddressUtils } from "./libraries/CrossDomainAddressUtils.sol";
import "./SpokePool.sol";

// https://github.com/matter-labs/era-contracts/blob/6391c0d7bf6184d7f6718060e3991ba6f0efe4a7/zksync/contracts/bridge/L2ERC20Bridge.sol#L104
interface ZkBridgeLike {
Expand All @@ -22,7 +24,9 @@ interface IL2ETH {
* @dev Resources for compiling and deploying contracts with hardhat: https://era.zksync.io/docs/tools/hardhat/hardhat-zksync-solc.html
* @custom:security-contact bugs@across.to
*/
contract ZkSync_SpokePool is SpokePool {
contract ZkSync_SpokePool is SpokePool, CircleCCTPAdapter {
using SafeERC20 for IERC20;

// On Ethereum, avoiding constructor parameters and putting them into constants reduces some of the gas cost
// upon contract deployment. On zkSync the opposite is true: deploying the same bytecode for contracts,
// while changing only constructor parameters can lead to substantial fee savings. So, the following params
Expand All @@ -33,18 +37,54 @@ contract ZkSync_SpokePool is SpokePool {

// Bridge used to withdraw ERC20's to L1
ZkBridgeLike public zkErc20Bridge;
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
ZkBridgeLike public immutable zkUSDCBridge;

event SetZkBridge(address indexed erc20Bridge, address indexed oldErc20Bridge);

error InvalidBridgeConfig();

/**
* @notice Constructor.
* @notice Circle bridged & native USDC are optionally supported via configuration, but are mutually exclusive.
* @param _wrappedNativeTokenAddress wrappedNativeToken address for this network to set.
* @param _circleUSDC Circle USDC address on the SpokePool. Set to 0x0 to use the standard ERC20 bridge instead.
* If not set to zero, then either the zkUSDCBridge or cctpTokenMessenger must be set and will be used to
* bridge this token.
* @param _zkUSDCBridge Elastic chain custom bridge address for USDC (if deployed, or address(0) to disable).
* @param _cctpTokenMessenger TokenMessenger contract to bridge via CCTP. If the zero address is passed, CCTP bridging will be disabled.
* @param _depositQuoteTimeBuffer depositQuoteTimeBuffer to set. Quote timestamps can't be set more than this amount
* into the past from the block time of the deposit.
* @param _fillDeadlineBuffer fillDeadlineBuffer to set. Fill deadlines can't be set more than this amount
* into the future from the block time of the deposit.
*/
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(
address _wrappedNativeTokenAddress,
IERC20 _circleUSDC,
ZkBridgeLike _zkUSDCBridge,
ITokenMessenger _cctpTokenMessenger,
uint32 _depositQuoteTimeBuffer,
uint32 _fillDeadlineBuffer
) SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer) {} // solhint-disable-line no-empty-blocks
)
SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer)
CircleCCTPAdapter(_circleUSDC, _cctpTokenMessenger, CircleDomainIds.Ethereum)
{
address zero = address(0);
if (address(_circleUSDC) != zero) {
bool zkUSDCBridgeDisabled = address(_zkUSDCBridge) == zero;
bool cctpUSDCBridgeDisabled = address(_cctpTokenMessenger) == zero;
// Bridged and Native USDC are mutually exclusive.
if (zkUSDCBridgeDisabled == cctpUSDCBridgeDisabled) {
revert InvalidBridgeConfig();
}
}

zkUSDCBridge = _zkUSDCBridge;
}

/**
* @notice Construct the ZkSync SpokePool.
* @notice Initialize the ZkSync SpokePool.
* @param _initialDepositId Starting deposit ID. Set to 0 unless this is a re-deployment in order to mitigate
* relay hash collisions.
* @param _zkErc20Bridge Address of L2 ERC20 gateway. Can be reset by admin.
Expand Down Expand Up @@ -109,6 +149,15 @@ contract ZkSync_SpokePool is SpokePool {
WETH9Interface(l2TokenAddress).withdraw(amountToReturn); // Unwrap into ETH.
// To withdraw tokens, we actually call 'withdraw' on the L2 eth token itself.
IL2ETH(l2Eth).withdraw{ value: amountToReturn }(withdrawalRecipient);
} else if (l2TokenAddress == address(usdcToken)) {
if (_isCCTPEnabled()) {
// Circle native USDC via CCTP.
_transferUsdc(withdrawalRecipient, amountToReturn);
} else {
// Matter Labs custom USDC bridge for Circle Bridged (upgradable) USDC.
IERC20(l2TokenAddress).forceApprove(address(zkUSDCBridge), amountToReturn);
zkUSDCBridge.withdraw(withdrawalRecipient, l2TokenAddress, amountToReturn);
}
} else {
zkErc20Bridge.withdraw(withdrawalRecipient, l2TokenAddress, amountToReturn);
}
Expand All @@ -121,4 +170,9 @@ contract ZkSync_SpokePool is SpokePool {
}

function _requireAdminSender() internal override onlyFromCrossDomainAdmin {}

// Reserve storage slots for future versions of this base contract to add state variables without
// affecting the storage layout of child contracts. Decrement the size of __gap whenever state variables
// are added. This is at bottom of contract to make sure its always at the end of storage.
uint256[999] private __gap;
}
59 changes: 51 additions & 8 deletions contracts/chain-adapters/ZkStack_Adapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import "../external/interfaces/WETH9Interface.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { BridgeHubInterface } from "../interfaces/ZkStackBridgeHub.sol";
import { CircleCCTPAdapter } from "../libraries/CircleCCTPAdapter.sol";
import { ITokenMessenger } from "../external/interfaces/CCTPInterfaces.sol";

/**
* @notice Contract containing logic to send messages from L1 to ZkStack with ETH as the gas token.
Expand All @@ -18,7 +20,7 @@ import { BridgeHubInterface } from "../interfaces/ZkStackBridgeHub.sol";
*/

// solhint-disable-next-line contract-name-camelcase
contract ZkStack_Adapter is AdapterInterface {
contract ZkStack_Adapter is AdapterInterface, CircleCCTPAdapter {
using SafeERC20 for IERC20;

// The ZkSync bridgehub contract treats address(1) to represent ETH.
Expand Down Expand Up @@ -49,8 +51,8 @@ contract ZkStack_Adapter is AdapterInterface {
// Set l1Weth at construction time to make testing easier.
WETH9Interface public immutable L1_WETH;

// SharedBridge address, which is read from the BridgeHub at construction.
address public immutable SHARED_BRIDGE;
// USDC SharedBridge address, which is passed in on construction and used as the second bridge contract for USDC transfers.
address public immutable USDC_SHARED_BRIDGE;

// The maximum gas price a transaction sent to this adapter may have. This is set to prevent a block producer from setting an artificially high priority fee
// when calling a hub pool message relay, which would otherwise cause a large amount of ETH to be sent to L2.
Expand All @@ -60,11 +62,18 @@ contract ZkStack_Adapter is AdapterInterface {

error ETHGasTokenRequired();
error TransactionFeeTooHigh();
error InvalidBridgeConfig();

/**
* @notice Constructs new Adapter.
* @notice Circle bridged & native USDC are optionally supported via configuration, but are mutually exclusive.
* @param _chainId The target ZkStack network's chain ID.
* @param _bridgeHub The bridge hub contract address for the ZkStack network.
* @param _circleUSDC Circle USDC address on L1. If not set to address(0), then either the USDCSharedBridge
* or CCTP token messenger must be set and will be used to bridge this token.
* @param _usdcSharedBridge Address of the second bridge contract for USDC corresponding to the configured ZkStack network.
* @param _cctpTokenMessenger address of the CCTP token messenger contract for the configured network.
* @param _recipientCircleDomainId Circle domain ID for the destination network.
* @param _l1Weth WETH address on L1.
* @param _l2RefundAddress address that recieves excess gas refunds on L2.
* @param _l2GasLimit The maximum amount of gas this contract is willing to pay to execute a transaction on L2.
Expand All @@ -74,20 +83,33 @@ contract ZkStack_Adapter is AdapterInterface {
constructor(
uint256 _chainId,
BridgeHubInterface _bridgeHub,
IERC20 _circleUSDC,
address _usdcSharedBridge,
ITokenMessenger _cctpTokenMessenger,
uint32 _recipientCircleDomainId,
WETH9Interface _l1Weth,
address _l2RefundAddress,
uint256 _l2GasLimit,
uint256 _l1GasToL2GasPerPubDataLimit,
uint256 _maxTxGasprice
) {
) CircleCCTPAdapter(_circleUSDC, _cctpTokenMessenger, _recipientCircleDomainId) {
CHAIN_ID = _chainId;
BRIDGE_HUB = _bridgeHub;
L1_WETH = _l1Weth;
L2_REFUND_ADDRESS = _l2RefundAddress;
L2_GAS_LIMIT = _l2GasLimit;
MAX_TX_GASPRICE = _maxTxGasprice;
L1_GAS_TO_L2_GAS_PER_PUB_DATA_LIMIT = _l1GasToL2GasPerPubDataLimit;
SHARED_BRIDGE = BRIDGE_HUB.sharedBridge();
address zero = address(0);
if (address(_circleUSDC) != zero) {
bool zkUSDCBridgeDisabled = _usdcSharedBridge == zero;
bool cctpUSDCBridgeDisabled = address(_cctpTokenMessenger) == zero;
// Bridged and Native USDC are mutually exclusive.
if (zkUSDCBridgeDisabled == cctpUSDCBridgeDisabled) {
revert InvalidBridgeConfig();
}
}
USDC_SHARED_BRIDGE = _usdcSharedBridge;
address gasToken = BRIDGE_HUB.baseToken(CHAIN_ID);
if (gasToken != ETH_TOKEN_ADDRESS) {
revert ETHGasTokenRequired();
Expand Down Expand Up @@ -158,9 +180,30 @@ contract ZkStack_Adapter is AdapterInterface {
refundRecipient: L2_REFUND_ADDRESS
})
);
} else if (l1Token == address(usdcToken)) {
// Either use CCTP or the custom shared bridge when bridging USDC.
if (_isCCTPEnabled()) {
_transferUsdc(to, amount);
} else {
IERC20(l1Token).forceApprove(USDC_SHARED_BRIDGE, amount);
txHash = BRIDGE_HUB.requestL2TransactionTwoBridges(
BridgeHubInterface.L2TransactionRequestTwoBridgesOuter({
chainId: CHAIN_ID,
mintValue: txBaseCost,
l2Value: 0,
l2GasLimit: L2_GAS_LIMIT,
l2GasPerPubdataByteLimit: L1_GAS_TO_L2_GAS_PER_PUB_DATA_LIMIT,
refundRecipient: L2_REFUND_ADDRESS,
secondBridgeAddress: USDC_SHARED_BRIDGE,
secondBridgeValue: 0,
secondBridgeCalldata: _secondBridgeCalldata(to, l1Token, amount)
})
);
}
} else {
// An ERC20 that is not WETH.
IERC20(l1Token).forceApprove(SHARED_BRIDGE, amount);
// An standard bridged ERC20, separate from WETH and Circle Bridged/Native USDC.
address sharedBridge = BRIDGE_HUB.sharedBridge();
IERC20(l1Token).forceApprove(sharedBridge, amount);
txHash = BRIDGE_HUB.requestL2TransactionTwoBridges{ value: txBaseCost }(
BridgeHubInterface.L2TransactionRequestTwoBridgesOuter({
chainId: CHAIN_ID,
Expand All @@ -169,7 +212,7 @@ contract ZkStack_Adapter is AdapterInterface {
l2GasLimit: L2_GAS_LIMIT,
l2GasPerPubdataByteLimit: L1_GAS_TO_L2_GAS_PER_PUB_DATA_LIMIT,
refundRecipient: L2_REFUND_ADDRESS,
secondBridgeAddress: SHARED_BRIDGE,
secondBridgeAddress: sharedBridge,
secondBridgeValue: 0,
secondBridgeCalldata: _secondBridgeCalldata(to, l1Token, amount)
})
Expand Down
Loading
Loading