Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6a535c8
feat: Support Lens Circle bridged USDC in SpokePool
pxrl Mar 27, 2025
b3027bf
immutable
pxrl Mar 27, 2025
5dbb876
Add zkSync contract gap
pxrl Mar 27, 2025
a14915f
Centralise defs
pxrl Mar 27, 2025
3759acb
Migrate to zkSync SpokePool
pxrl Mar 27, 2025
a99ab0e
Update
pxrl Mar 27, 2025
cfe0393
update
pxrl Mar 27, 2025
5f4a975
Update contracts/ZkSync_SpokePool.sol
pxrl Mar 27, 2025
3ca2e2a
revert unnecessary change
pxrl Mar 27, 2025
d940e3a
Update comments
pxrl Mar 27, 2025
3a639e2
Fix deployment script
pxrl Mar 27, 2025
850aeeb
Simplify
pxrl Mar 27, 2025
6052428
Update comments
pxrl Mar 27, 2025
6bffd0d
Update import
pxrl Mar 27, 2025
96f78f9
fixes
pxrl Mar 27, 2025
3f700bc
improve: support upgradeable USDC in zkstack adapters (#940)
bmzig Mar 28, 2025
4680216
Update initial test
pxrl Mar 27, 2025
745e8cb
Update zkSync SpokePool storage
pxrl Mar 28, 2025
e509dde
Extra test
pxrl Mar 28, 2025
eb6731b
Update test/evm/hardhat/chain-specific-spokepools/ZkSync_SpokePool.ts
pxrl Mar 28, 2025
a9d7c3e
Apply suggestions from code review
pxrl Mar 30, 2025
ce79cf6
Harden constructor
pxrl Apr 1, 2025
def3a9b
Expand & fix
pxrl Apr 1, 2025
4d57e9b
fix
pxrl Apr 1, 2025
844e391
Apply suggestions from code review
pxrl Apr 1, 2025
bad769b
Add constructor checks to Adapters, update deploy script
nicholaspai Apr 2, 2025
a1408ae
Update 059_deploy_lens_spokepool.ts
nicholaspai Apr 2, 2025
594a6eb
Update contracts/ZkSync_SpokePool.sol
nicholaspai Apr 2, 2025
6dd59aa
Clarify constructor arguments for zkSync
pxrl Apr 2, 2025
341c6ff
readability in adapters
nicholaspai Apr 2, 2025
4fe0076
Update deploy/059_deploy_lens_spokepool.ts
nicholaspai Apr 2, 2025
7b64ff2
Merge branch 'pxrl/lensUSDC' of https://github.com/across-protocol/co…
nicholaspai Apr 2, 2025
ff9af3b
Move asserts
nicholaspai Apr 2, 2025
ffeaade
Remove if (address(_l2Usdc) != zero)
nicholaspai Apr 2, 2025
f644402
Fix deploy assert
nicholaspai Apr 2, 2025
7f35c2d
Revert "Remove if (address(_l2Usdc) != zero)"
nicholaspai Apr 2, 2025
ceb8cbe
Rename constructor param
nicholaspai Apr 2, 2025
78063ab
Merge branch 'audit-zkstack-usdc' into pxrl/lensUSDC
nicholaspai Apr 8, 2025
bedc62a
Remove SHARED_BRIDGE
nicholaspai Apr 8, 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
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
)
{}
}
55 changes: 52 additions & 3 deletions contracts/ZkSync_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.0;

import "./SpokePool.sol";
import { CircleCCTPAdapter, CircleDomainIds, ITokenMessenger } from "./libraries/CircleCCTPAdapter.sol";
import { CrossDomainAddressUtils } from "./libraries/CrossDomainAddressUtils.sol";

// https://github.com/matter-labs/era-contracts/blob/6391c0d7bf6184d7f6718060e3991ba6f0efe4a7/zksync/contracts/bridge/L2ERC20Bridge.sol#L104
Expand All @@ -22,7 +23,7 @@ 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 {
// 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 +34,53 @@ 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.
* @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 +145,14 @@ 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.
zkUSDCBridge.withdraw(withdrawalRecipient, l2TokenAddress, amountToReturn);
}
} else {
zkErc20Bridge.withdraw(withdrawalRecipient, l2TokenAddress, amountToReturn);
}
Expand All @@ -121,4 +165,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;
}
49 changes: 47 additions & 2 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,6 +51,9 @@ contract ZkStack_Adapter is AdapterInterface {
// Set l1Weth at construction time to make testing easier.
WETH9Interface public immutable L1_WETH;

// 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.
uint256 private immutable MAX_TX_GASPRICE;
Expand All @@ -57,11 +62,17 @@ contract ZkStack_Adapter is AdapterInterface {

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

/**
* @notice Constructs new Adapter.
* @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 @@ -71,19 +82,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;
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 @@ -154,6 +179,26 @@ 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.
address sharedBridge = BRIDGE_HUB.sharedBridge();
Expand Down
49 changes: 47 additions & 2 deletions contracts/chain-adapters/ZkStack_CustomGasToken_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 Interface for funder contract that this contract pulls from to pay for relayMessage()/relayTokens()
Expand All @@ -33,7 +35,7 @@ interface FunderInterface {
*/

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

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

// 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;

// Custom gas token address, which is read from the BridgeHub at construction.
address public immutable CUSTOM_GAS_TOKEN;

Expand All @@ -77,11 +82,17 @@ contract ZkStack_CustomGasToken_Adapter is AdapterInterface {
event ZkStackMessageRelayed(bytes32 indexed canonicalTxHash);
error ETHGasTokenNotAllowed();
error TransactionFeeTooHigh();
error InvalidBridgeConfig();

/**
* @notice Constructs new Adapter.
* @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 _customGasTokenFunder Contract on L1 which funds bridge fees with amounts in the custom gas token.
Expand All @@ -92,13 +103,17 @@ contract ZkStack_CustomGasToken_Adapter is AdapterInterface {
constructor(
uint256 _chainId,
BridgeHubInterface _bridgeHub,
IERC20 _circleUSDC,
address _usdcSharedBridge,
ITokenMessenger _cctpTokenMessenger,
uint32 _recipientCircleDomainId,
WETH9Interface _l1Weth,
address _l2RefundAddress,
FunderInterface _customGasTokenFunder,
uint256 _l2GasLimit,
uint256 _l1GasToL2GasPerPubDataLimit,
uint256 _maxTxGasprice
) {
) CircleCCTPAdapter(_circleUSDC, _cctpTokenMessenger, _recipientCircleDomainId) {
CHAIN_ID = _chainId;
BRIDGE_HUB = _bridgeHub;
L1_WETH = _l1Weth;
Expand All @@ -107,6 +122,16 @@ contract ZkStack_CustomGasToken_Adapter is AdapterInterface {
L2_GAS_LIMIT = _l2GasLimit;
MAX_TX_GASPRICE = _maxTxGasprice;
L1_GAS_TO_L2_GAS_PER_PUB_DATA_LIMIT = _l1GasToL2GasPerPubDataLimit;
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;
CUSTOM_GAS_TOKEN = BRIDGE_HUB.baseToken(CHAIN_ID);
if (CUSTOM_GAS_TOKEN == ETH_TOKEN_ADDRESS) {
revert ETHGasTokenNotAllowed();
Expand Down Expand Up @@ -198,6 +223,26 @@ contract ZkStack_CustomGasToken_Adapter is AdapterInterface {
refundRecipient: L2_REFUND_ADDRESS
})
);
} else if (l1Token == address(usdcToken)) {
if (_isCCTPEnabled()) {
_transferUsdc(to, amount);
} else {
IERC20(CUSTOM_GAS_TOKEN).forceApprove(USDC_SHARED_BRIDGE, txBaseCost);
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 and not the custom gas token.
IERC20(CUSTOM_GAS_TOKEN).forceApprove(sharedBridge, txBaseCost);
Expand Down
29 changes: 26 additions & 3 deletions deploy/016_deploy_zksync_spokepool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction, DeploymentSubmission } from "hardhat-deploy/types";
import { getDeployedAddress } from "../src/DeploymentUtils";
import { getSpokePoolDeploymentInfo } from "../utils/utils.hre";
import { FILL_DEADLINE_BUFFER, L2_ADDRESS_MAP, QUOTE_TIME_BUFFER, WETH } from "./consts";
import { FILL_DEADLINE_BUFFER, L2_ADDRESS_MAP, QUOTE_TIME_BUFFER, USDC, WETH, ZERO_ADDRESS } from "./consts";
import assert from "assert";

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const contractName = "ZkSync_SpokePool";
Expand All @@ -18,13 +19,35 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const deployer = new zkDeployer(hre, wallet);

const artifact = await deployer.loadArtifact(contractName);

const { zkErc20Bridge, zkUSDCBridge = ZERO_ADDRESS, cctpTokenMessenger } = L2_ADDRESS_MAP[spokeChainId];
const initArgs = [
0, // Start at 0 since this first time we're deploying this spoke pool. On future upgrades increase this.
L2_ADDRESS_MAP[spokeChainId].zkErc20Bridge,
zkErc20Bridge,
hubPool.address,
hubPool.address,
];
const constructorArgs = [WETH[spokeChainId], QUOTE_TIME_BUFFER, FILL_DEADLINE_BUFFER];

// Set USDC address to 0x0 to bypass special handling for USDC and revert to standard zkSync ERC20 bridge.
// When CCTP is available, set the new addresses for cctpTokenMessenger and USDC to enable.
const usdcAddress =
zkUSDCBridge === ZERO_ADDRESS && cctpTokenMessenger === ZERO_ADDRESS ? ZERO_ADDRESS : USDC[spokeChainId];
if (usdcAddress !== ZERO_ADDRESS) {
const cctpTokenMessengerDefined = cctpTokenMessenger !== ZERO_ADDRESS;
const zkUSDCBridgeDefined = zkUSDCBridge !== ZERO_ADDRESS;
assert(
cctpTokenMessengerDefined !== zkUSDCBridgeDefined,
"Only one of zkUSDCBridge and cctpTokenMessenger should be set to a non-zero address"
);
}
const constructorArgs = [
WETH[spokeChainId],
usdcAddress,
zkUSDCBridge,
cctpTokenMessenger,
QUOTE_TIME_BUFFER,
FILL_DEADLINE_BUFFER,
];

let newAddress: string;
// On production, we'll rarely want to deploy a new proxy contract so we'll default to deploying a new implementation
Expand Down
Loading
Loading